diff --git a/.docker/dev/Dockerfile b/.docker/dev/Dockerfile index ca8b46fba5..eb41c0ebf1 100644 --- a/.docker/dev/Dockerfile +++ b/.docker/dev/Dockerfile @@ -1,6 +1,6 @@ FROM sylius/standard:1.11-traditional -RUN apt-get update && apt-get install php8.0-xdebug && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* +RUN apt-get update && apt-get install curl php8.0-xdebug -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* RUN version=$(php -r "echo PHP_MAJOR_VERSION.PHP_MINOR_VERSION;") \ && architecture=$(uname -m) \ diff --git a/.gitattributes b/.gitattributes index f2fdfcc779..de3efce569 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,8 +1,41 @@ -/.docker export-ignore -/.github export-ignore -/adr export-ignore -/bin export-ignore -/config export-ignore -/docs export-ignore -/public export-ignore -docker-compose.*.yml export-ignore +/.bunnyshell export-ignore +/.docker export-ignore +/.github export-ignore +/adr export-ignore +/bin export-ignore +/config export-ignore +/docs export-ignore +/etd/build export-ignore +/public export-ignore +/templates export-ignore +/tests export-ignore +/translations export-ignore +/var export-ignore +.babelrc export-ignore +.dockerignore export-ignore +.editorconfig export-ignore +.env export-ignore +.env.* export-ignore +.eslintrc.js export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.npmignore export-ignore +.readthedocs.yaml export-ignore +.symfony.insight.yaml export-ignore +composer-require-checker.json export-ignore +docker-compose.* export-ignore +ecs.php export-ignore +gulpfile.babel.js export-ignore +Makefile export-ignore +monorepo-builder.php export-ignore +package.json export-ignore +phparkitect.php export-ignore +phpspec.yml.dist export-ignore +phpstan.neon.dist export-ignore +phpstan-baseline.neon export-ignore +phpunit.xml.dist export-ignore +rector.php export-ignore +RoboFile.php export-ignore +symfony.lock export-ignore +webpack.config.js export-ignore +yaml-standards.yaml export-ignore diff --git a/.github/workflows/auto-merge.yaml b/.github/workflows/auto-merge.yaml index d98fa6c652..f7e45f030a 100644 --- a/.github/workflows/auto-merge.yaml +++ b/.github/workflows/auto-merge.yaml @@ -3,11 +3,16 @@ name: Auto-merge on: pull_request: ~ +permissions: + contents: read + jobs: auto-merge: + permissions: + pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Auto-merge minor dependencies upgrades diff --git a/.github/workflows/ci__full_1_12.yaml b/.github/workflows/ci__full_1_12.yaml new file mode 100644 index 0000000000..c455213f2c --- /dev/null +++ b/.github/workflows/ci__full_1_12.yaml @@ -0,0 +1,131 @@ +name: Continuous Integration 1.12 (Full) + +on: + schedule: + - + cron: "0 1 * * *" # Run every day at 1am + workflow_dispatch: ~ + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }}-1_12-full + cancel-in-progress: true + +permissions: + contents: read + +jobs: + static-checks: + strategy: + matrix: + branch: [ "1.12" ] + name: "[${{ matrix.branch }}] Static checks" + uses: ./.github/workflows/ci_static-checks.yaml + with: + branch: ${{ matrix.branch }} + type: full + e2e-mariadb: + strategy: + matrix: + branch: [ "1.12" ] + name: "[${{ matrix.branch }}] End-to-end tests (MariaDB)" + needs: static-checks + uses: ./.github/workflows/ci_e2e-mariadb.yaml + with: + branch: ${{ matrix.branch }} + type: full + e2e-mysql: + strategy: + matrix: + branch: [ "1.12" ] + name: "[${{ matrix.branch }}] End-to-end tests (MySQL)" + needs: static-checks + uses: ./.github/workflows/ci_e2e-mysql.yaml + with: + branch: ${{ matrix.branch }} + type: full + e2e-pgsql: + strategy: + matrix: + branch: [ "1.12" ] + name: "[${{ matrix.branch }}] End-to-end tests (PostgreSQL)" + needs: static-checks + uses: ./.github/workflows/ci_e2e-pgsql.yaml + with: + branch: ${{ matrix.branch }} + type: full + e2e-custom: + strategy: + matrix: + branch: [ "1.12" ] + name: "[${{ matrix.branch }}] End-to-end tests (Custom)" + needs: static-checks + uses: ./.github/workflows/ci_e2e-custom.yaml + with: + branch: ${{ matrix.branch }} + frontend: + name: Frontend + needs: static-checks + uses: ./.github/workflows/ci_frontend.yaml + with: + branch: ${{ matrix.branch }} + type: full + packages: + strategy: + matrix: + branch: [ "1.12" ] + name: "[${{ matrix.branch }}] Packages" + needs: static-checks + uses: ./.github/workflows/ci_packages.yaml + with: + branch: ${{ matrix.branch }} + type: full + unstable: + name: Unstable + needs: static-checks + uses: ./.github/workflows/ci__unstable.yaml + with: + ignore-failure: false + notify-about-build-status: + if: ${{ always() }} + name: "Notify about build status" + needs: [static-checks, e2e-mariadb, e2e-mysql, e2e-pgsql, e2e-custom, packages, unstable] + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: "Process data" + id: process-data + shell: bash + run: | + echo "branch=$(echo ${{ github.ref }} | sed 's/refs\/heads\///g' | sed 's/refs\/tags\///g')" >> $GITHUB_OUTPUT + echo "sha=$(echo ${{ github.sha }} | cut -c 1-12)" >> $GITHUB_OUTPUT + + - name: "Notify on Slack" + uses: edge/simple-slack-notify@master + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + if: env.SLACK_WEBHOOK_URL != null + with: + channel: "#daily-build" + username: "GitHub Actions" + text: | + ** + + ${{ needs.static-checks.result == 'success' && ':+1:' || ':x:' }} Static Checks + ${{ needs.e2e-mariadb.result == 'success' && ':+1:' || ':x:' }} End-to-End (MariaDB) + ${{ needs.e2e-mysql.result == 'success' && ':+1:' || ':x:' }} End-to-End (MySQL) + ${{ needs.e2e-pgsql.result == 'success' && ':+1:' || ':x:' }} End-to-End (PostgreSQL) + ${{ needs.e2e-custom.result == 'success' && ':+1:' || ':x:' }} End-to-End (Custom) + ${{ needs.packages.result == 'success' && ':+1:' || ':x:' }} Packages + ${{ needs.unstable.result == 'success' && ':+1:' || ':x:' }} Unstable + + _ _ _ _ _ _ _ + color: "danger" + fields: | + [ + { "title": "Repository", "value": "", "short": true }, + { "title": "Action", "value": "", "short": true }, + { "title": "Reference", "value": "", "short": true }, + { "title": "Commit", "value": "", "short": true }, + { "title": "Event", "value": "${{ github.event_name }}", "short": true } + ] diff --git a/.github/workflows/ci__full_1_13.yaml b/.github/workflows/ci__full_1_13.yaml new file mode 100644 index 0000000000..673ed26223 --- /dev/null +++ b/.github/workflows/ci__full_1_13.yaml @@ -0,0 +1,131 @@ +name: Continuous Integration 1.13 (Full) + +on: + schedule: + - + cron: "0 2 * * *" # Run every day at 2am + workflow_dispatch: ~ + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }}-1_13-full + cancel-in-progress: true + +permissions: + contents: read + +jobs: + static-checks: + strategy: + matrix: + branch: [ "1.13" ] + name: "[${{ matrix.branch }}] Static checks" + uses: ./.github/workflows/ci_static-checks.yaml + with: + branch: ${{ matrix.branch }} + type: full + e2e-mariadb: + strategy: + matrix: + branch: [ "1.13" ] + name: "[${{ matrix.branch }}] End-to-end tests (MariaDB)" + needs: static-checks + uses: ./.github/workflows/ci_e2e-mariadb.yaml + with: + branch: ${{ matrix.branch }} + type: full + e2e-mysql: + strategy: + matrix: + branch: [ "1.13" ] + name: "[${{ matrix.branch }}] End-to-end tests (MySQL)" + needs: static-checks + uses: ./.github/workflows/ci_e2e-mysql.yaml + with: + branch: ${{ matrix.branch }} + type: full + e2e-pgsql: + strategy: + matrix: + branch: [ "1.13" ] + name: "[${{ matrix.branch }}] End-to-end tests (PostgreSQL)" + needs: static-checks + uses: ./.github/workflows/ci_e2e-pgsql.yaml + with: + branch: ${{ matrix.branch }} + type: full + e2e-custom: + strategy: + matrix: + branch: [ "1.13" ] + name: "[${{ matrix.branch }}] End-to-end tests (Custom)" + needs: static-checks + uses: ./.github/workflows/ci_e2e-custom.yaml + with: + branch: ${{ matrix.branch }} + frontend: + name: Frontend + needs: static-checks + uses: ./.github/workflows/ci_frontend.yaml + with: + branch: ${{ matrix.branch }} + type: full + packages: + strategy: + matrix: + branch: [ "1.13" ] + name: "[${{ matrix.branch }}] Packages" + needs: static-checks + uses: ./.github/workflows/ci_packages.yaml + with: + branch: ${{ matrix.branch }} + type: full + unstable: + name: Unstable + needs: static-checks + uses: ./.github/workflows/ci__unstable.yaml + with: + ignore-failure: false + notify-about-build-status: + if: ${{ always() }} + name: "Notify about build status" + needs: [static-checks, e2e-mariadb, e2e-mysql, e2e-pgsql, e2e-custom, packages, unstable] + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: "Process data" + id: process-data + shell: bash + run: | + echo "branch=$(echo ${{ github.ref }} | sed 's/refs\/heads\///g' | sed 's/refs\/tags\///g')" >> $GITHUB_OUTPUT + echo "sha=$(echo ${{ github.sha }} | cut -c 1-12)" >> $GITHUB_OUTPUT + + - name: "Notify on Slack" + uses: edge/simple-slack-notify@master + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + if: env.SLACK_WEBHOOK_URL != null + with: + channel: "#daily-build" + username: "GitHub Actions" + text: | + ** + + ${{ needs.static-checks.result == 'success' && ':+1:' || ':x:' }} Static Checks + ${{ needs.e2e-mariadb.result == 'success' && ':+1:' || ':x:' }} End-to-End (MariaDB) + ${{ needs.e2e-mysql.result == 'success' && ':+1:' || ':x:' }} End-to-End (MySQL) + ${{ needs.e2e-pgsql.result == 'success' && ':+1:' || ':x:' }} End-to-End (PostgreSQL) + ${{ needs.e2e-custom.result == 'success' && ':+1:' || ':x:' }} End-to-End (Custom) + ${{ needs.packages.result == 'success' && ':+1:' || ':x:' }} Packages + ${{ needs.unstable.result == 'success' && ':+1:' || ':x:' }} Unstable + + _ _ _ _ _ _ _ + color: "danger" + fields: | + [ + { "title": "Repository", "value": "", "short": true }, + { "title": "Action", "value": "", "short": true }, + { "title": "Reference", "value": "", "short": true }, + { "title": "Commit", "value": "", "short": true }, + { "title": "Event", "value": "${{ github.event_name }}", "short": true } + ] diff --git a/.github/workflows/ci__full.yaml b/.github/workflows/ci__full_2_0.yaml similarity index 75% rename from .github/workflows/ci__full.yaml rename to .github/workflows/ci__full_2_0.yaml index 0dcb58edce..84ae2a22bd 100644 --- a/.github/workflows/ci__full.yaml +++ b/.github/workflows/ci__full_2_0.yaml @@ -1,51 +1,83 @@ -name: Continuous Integration (Full) +name: Continuous Integration 2.0 (Full) on: + schedule: + - + cron: "0 3 * * *" # Run every day at 3am workflow_dispatch: ~ concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }}-full + group: ci-${{ github.workflow }}-${{ github.ref }}-2_0-full cancel-in-progress: true +permissions: + contents: read + jobs: static-checks: - name: Static checks + strategy: + matrix: + branch: [ "2.0" ] + name: "[${{ matrix.branch }}] Static checks" uses: ./.github/workflows/ci_static-checks.yaml with: + branch: ${{ matrix.branch }} type: full e2e-mariadb: - name: End-to-end tests (MariaDB) + strategy: + matrix: + branch: [ "2.0" ] + name: "[${{ matrix.branch }}] End-to-end tests (MariaDB)" needs: static-checks uses: ./.github/workflows/ci_e2e-mariadb.yaml with: + branch: ${{ matrix.branch }} type: full e2e-mysql: - name: End-to-end tests (MySQL) + strategy: + matrix: + branch: [ "2.0" ] + name: "[${{ matrix.branch }}] End-to-end tests (MySQL)" needs: static-checks uses: ./.github/workflows/ci_e2e-mysql.yaml with: + branch: ${{ matrix.branch }} type: full e2e-pgsql: - name: End-to-end tests (PostgreSQL) + strategy: + matrix: + branch: [ "2.0" ] + name: "[${{ matrix.branch }}] End-to-end tests (PostgreSQL)" needs: static-checks uses: ./.github/workflows/ci_e2e-pgsql.yaml with: + branch: ${{ matrix.branch }} type: full e2e-custom: - name: End-to-end tests (Custom) + strategy: + matrix: + branch: [ "2.0" ] + name: "[${{ matrix.branch }}] End-to-end tests (Custom)" needs: static-checks uses: ./.github/workflows/ci_e2e-custom.yaml + with: + branch: ${{ matrix.branch }} frontend: name: Frontend needs: static-checks uses: ./.github/workflows/ci_frontend.yaml with: + branch: ${{ matrix.branch }} type: full packages: - name: Packages + strategy: + matrix: + branch: [ "2.0" ] + name: "[${{ matrix.branch }}] Packages" needs: static-checks uses: ./.github/workflows/ci_packages.yaml with: + branch: ${{ matrix.branch }} type: full unstable: name: Unstable diff --git a/.github/workflows/ci__minimal.yaml b/.github/workflows/ci__minimal.yaml index 93912dbb5e..12697f6d69 100644 --- a/.github/workflows/ci__minimal.yaml +++ b/.github/workflows/ci__minimal.yaml @@ -9,12 +9,16 @@ on: workflow_dispatch: ~ push: branches-ignore: + - 'dependabot/**' - 'upmerge/**' - + concurrency: group: ci-${{ github.workflow }}-${{ github.ref }}-minimal cancel-in-progress: true +permissions: + contents: read + jobs: static-checks: name: Static checks diff --git a/.github/workflows/ci__unstable.yaml b/.github/workflows/ci__unstable.yaml index 1135955341..b3173a7f96 100644 --- a/.github/workflows/ci__unstable.yaml +++ b/.github/workflows/ci__unstable.yaml @@ -19,6 +19,9 @@ concurrency: group: ci-${{ github.workflow }}-${{ github.ref }}-unstable cancel-in-progress: true +permissions: + contents: read + jobs: e2e-unstable: name: End-to-end tests (Unstable) diff --git a/.github/workflows/ci_e2e-custom.yaml b/.github/workflows/ci_e2e-custom.yaml index 97b6e077fb..8b4c930704 100644 --- a/.github/workflows/ci_e2e-custom.yaml +++ b/.github/workflows/ci_e2e-custom.yaml @@ -2,7 +2,16 @@ name: End-to-End (Custom) on: workflow_dispatch: ~ - workflow_call: ~ + workflow_call: + inputs: + branch: + description: "Branch" + required: false + type: string + default: "" + +permissions: + contents: read jobs: behat-no-js-unstable-symfony: @@ -22,8 +31,26 @@ jobs: DATABASE_URL: "mysql://root:root@127.0.0.1/sylius?charset=utf8mb4&serverVersion=${{ matrix.mysql }}" steps: - - - uses: actions/checkout@v3 + - name: Set variables + shell: bash + env: + BRANCH: ${{ inputs.branch }} + run: | + if [ "$BRANCH" == "1.12" ]; then + echo "USE_LEGACY_POSTGRES_SETUP=yes" >> $GITHUB_ENV + else + echo "USE_LEGACY_POSTGRES_SETUP=no" >> $GITHUB_ENV + fi + + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 - name: Change minimum-stability to dev run: | @@ -31,16 +58,17 @@ jobs: composer config prefer-stable true - name: Build application - uses: SyliusLabs/BuildTestAppAction@v2.0 + uses: SyliusLabs/BuildTestAppAction@v2.2 with: build_type: "sylius" cache_key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-" cache_restore_key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-" e2e: "yes" database_version: ${{ matrix.mysql }} + legacy_postgresql_setup: ${{ env.USE_LEGACY_POSTGRES_SETUP }} php_version: ${{ matrix.php }} symfony_version: ${{ matrix.symfony }} - + - name: Run PHPUnit continue-on-error: true run: vendor/bin/phpunit --colors=always diff --git a/.github/workflows/ci_e2e-mariadb.yaml b/.github/workflows/ci_e2e-mariadb.yaml index 2316c9ee06..8cf0f5dc17 100644 --- a/.github/workflows/ci_e2e-mariadb.yaml +++ b/.github/workflows/ci_e2e-mariadb.yaml @@ -4,11 +4,19 @@ on: workflow_dispatch: ~ workflow_call: inputs: + branch: + description: "Branch" + required: false + type: string + default: "" type: description: "Type of the build" required: true type: string +permissions: + contents: read + jobs: get-matrix: runs-on: ubuntu-latest @@ -16,9 +24,17 @@ jobs: outputs: matrix: ${{ steps.matrix.outputs.prop }} steps: - - uses: actions/checkout@v3 - - - name: "Get matrix" + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + uses: actions/checkout@v4 + if: "${{ inputs.branch == '' }}" + + - name: "Get matrix" id: matrix uses: notiz-dev/github-action-json-property@release with: @@ -28,7 +44,7 @@ jobs: behat-no-js: needs: get-matrix runs-on: ubuntu-latest - name: "Non-JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MariaDB ${{ matrix.mariadb }}, DBAL ${{ matrix.dbal }}" + name: "Non-JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MariaDB ${{ matrix.mariadb }}, State Machine Adapter ${{ matrix.state_machine_adapter }}" timeout-minutes: 45 strategy: fail-fast: false @@ -37,17 +53,32 @@ jobs: env: APP_ENV: test_cached DATABASE_URL: "mysql://root:root@127.0.0.1/sylius?charset=utf8mb4&serverVersion=mariadb-${{ matrix.mariadb }}" + TEST_SYLIUS_STATE_MACHINE_ADAPTER: "${{ matrix.state_machine_adapter }}" steps: - - - uses: actions/checkout@v3 - - - name: Restrict packages - if: matrix.dbal == '^2.7' - run: composer require --no-update --no-scripts --no-interaction "doctrine/dbal:${{ matrix.dbal }}" + - name: Set variables + shell: bash + env: + BRANCH: ${{ inputs.branch }} + run: | + if [ "$BRANCH" == "1.12" ]; then + echo "USE_LEGACY_POSTGRES_SETUP=yes" >> $GITHUB_ENV + else + echo "USE_LEGACY_POSTGRES_SETUP=no" >> $GITHUB_ENV + fi + + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 - name: Build application - uses: SyliusLabs/BuildTestAppAction@v2.0 + uses: SyliusLabs/BuildTestAppAction@v2.2 with: build_type: "sylius" cache_key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-" @@ -55,6 +86,7 @@ jobs: e2e: "yes" database: "mariadb" database_version: ${{ matrix.mariadb }} + legacy_postgresql_setup: ${{ env.USE_LEGACY_POSTGRES_SETUP }} php_version: ${{ matrix.php }} symfony_version: ${{ matrix.symfony }} @@ -74,7 +106,7 @@ jobs: uses: actions/upload-artifact@v3 if: failure() with: - name: "Logs (non-JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MariaDB ${{ matrix.mariadb }}), DBAL ${{ matrix.dbal }}" + name: "Logs (non-JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MariaDB ${{ matrix.mariadb }})" path: | etc/build/ var/log diff --git a/.github/workflows/ci_e2e-mysql.yaml b/.github/workflows/ci_e2e-mysql.yaml index 36d61dd349..d4c451970f 100644 --- a/.github/workflows/ci_e2e-mysql.yaml +++ b/.github/workflows/ci_e2e-mysql.yaml @@ -4,11 +4,19 @@ on: workflow_dispatch: ~ workflow_call: inputs: + branch: + description: "Branch" + required: false + type: string + default: "" type: description: "Type of the build" required: true type: string +permissions: + contents: read + jobs: get-matrix: runs-on: ubuntu-latest @@ -16,7 +24,15 @@ jobs: outputs: matrix: ${{ steps.matrix.outputs.prop }} steps: - - uses: actions/checkout@v3 + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + uses: actions/checkout@v4 + if: "${{ inputs.branch == '' }}" - name: "Get matrix" id: matrix @@ -28,7 +44,7 @@ jobs: behat-no-js: needs: get-matrix runs-on: ubuntu-latest - name: "Non-JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}, Twig ${{ matrix.twig }}, DBAL ${{ matrix.dbal }}.x, API Platform ${{ matrix.api-platform }}" + name: "Non-JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}, Twig ${{ matrix.twig }}, API Platform ${{ matrix.api-platform }}" timeout-minutes: 45 strategy: fail-fast: false @@ -39,25 +55,40 @@ jobs: DATABASE_URL: "mysql://root:root@127.0.0.1/sylius?charset=utf8mb4&serverVersion=${{ matrix.mysql }}" steps: - - - uses: actions/checkout@v3 + - name: Set variables + shell: bash + env: + BRANCH: ${{ inputs.branch }} + run: | + if [ "$BRANCH" == "1.12" ]; then + echo "USE_LEGACY_POSTGRES_SETUP=yes" >> $GITHUB_ENV + else + echo "USE_LEGACY_POSTGRES_SETUP=no" >> $GITHUB_ENV + fi + + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 - name: Restrict Twig if: matrix.twig == '^2.12' run: composer require --no-update --no-scripts --no-interaction "twig/twig:${{ matrix.twig }}" - - - name: Restrict DBAL - if: matrix.dbal == '^2.7' - run: composer require --no-update --no-scripts --no-interaction "doctrine/dbal:${{ matrix.dbal }}" - + - name: Build application - uses: SyliusLabs/BuildTestAppAction@v2.0 + uses: SyliusLabs/BuildTestAppAction@v2.2 with: build_type: "sylius" cache_key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-symfony-${{ matrix.symfony }}-api-platform-${{ matrix.api-platform }}" cache_restore_key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-symfony-${{ matrix.symfony }}-api-platform-${{ matrix.api-platform }}" e2e: "yes" database_version: ${{ matrix.mysql }} + legacy_postgresql_setup: ${{ env.USE_LEGACY_POSTGRES_SETUP }} php_version: ${{ matrix.php }} symfony_version: ${{ matrix.symfony }} @@ -80,7 +111,7 @@ jobs: uses: actions/upload-artifact@v3 if: failure() with: - name: "Logs (non-JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}), ApiPlatform ${{ matrix.api-platform }}, Twig ${{ matrix.twig }}, MySQL ${{ matrix.mysql }}, DBAL ${{ matrix.dbal }}" + name: "Logs (non-JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}), ApiPlatform ${{ matrix.api-platform }}, Twig ${{ matrix.twig }}, MySQL ${{ matrix.mysql }}" path: | etc/build/ var/log @@ -89,7 +120,7 @@ jobs: behat-ui-js: needs: get-matrix runs-on: ubuntu-latest - name: "JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}, Twig ${{ matrix.twig }}, DBAL ${{ matrix.dbal }}, API Platform ${{ matrix.api-platform }}" + name: "JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, MySQL ${{ matrix.mysql }}, Twig ${{ matrix.twig }}, API Platform ${{ matrix.api-platform }}" timeout-minutes: 45 strategy: fail-fast: false @@ -100,8 +131,26 @@ jobs: DATABASE_URL: "mysql://root:root@127.0.0.1/sylius?charset=utf8mb4&serverVersion=${{ matrix.mysql }}" steps: - - - uses: actions/checkout@v3 + - name: Set variables + shell: bash + env: + BRANCH: ${{ inputs.branch }} + run: | + if [ "$BRANCH" == "1.12" ]; then + echo "USE_LEGACY_POSTGRES_SETUP=yes" >> $GITHUB_ENV + else + echo "USE_LEGACY_POSTGRES_SETUP=no" >> $GITHUB_ENV + fi + + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 - name: Get Composer cache directory id: composer-cache @@ -118,12 +167,8 @@ jobs: if: matrix.twig == '^2.12' run: composer require --no-update --no-scripts --no-interaction "twig/twig:${{ matrix.twig }}" - - name: Restrict DBAL - if: matrix.dbal == '^2.7' - run: composer require --no-update --no-scripts --no-interaction "doctrine/dbal:${{ matrix.dbal }}" - - name: Build application - uses: jakubtobiasz/SyliusBuildTestAppAction@v2.0 + uses: SyliusLabs/BuildTestAppAction@v2.2 with: build_type: "sylius" cache_key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-symfony-${{ matrix.symfony }}-api-platform-${{ matrix.api-platform }}" @@ -131,12 +176,14 @@ jobs: e2e: "yes" e2e_js: "yes" database_version: ${{ matrix.mysql }} + legacy_postgresql_setup: ${{ env.USE_LEGACY_POSTGRES_SETUP }} php_version: ${{ matrix.php }} symfony_version: ${{ matrix.symfony }} + node_version: "20.x" - name: Install Behat driver run: vendor/bin/bdi detect drivers - + - name: Run Behat (Chromedriver) run: vendor/bin/behat --colors --strict --no-interaction -vvv -f progress --tags="@mink:chromedriver&&~@todo&&~@cli" --suite-tags="@ui" || vendor/bin/behat --colors --strict --no-interaction -vvv -f progress --tags="@mink:chromedriver&&~@todo&&~@cli" --suite-tags="@ui" --rerun @@ -147,7 +194,7 @@ jobs: uses: actions/upload-artifact@v3 if: failure() with: - name: "Logs (JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, ApiPlatform ${{ matrix.api-platform }}, Twig ${{ matrix.twig }}, MySQL ${{ matrix.mysql }}, DBAL ${{ matrix.dbal }})" + name: "Logs (JS, PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }}, ApiPlatform ${{ matrix.api-platform }}, Twig ${{ matrix.twig }}, MySQL ${{ matrix.mysql }})" path: | etc/build/ var/log diff --git a/.github/workflows/ci_e2e-pgsql.yaml b/.github/workflows/ci_e2e-pgsql.yaml index 0593f6a3df..2c4e7ba1da 100644 --- a/.github/workflows/ci_e2e-pgsql.yaml +++ b/.github/workflows/ci_e2e-pgsql.yaml @@ -4,11 +4,19 @@ on: workflow_dispatch: ~ workflow_call: inputs: + branch: + description: "Branch" + required: false + type: string + default: "" type: description: "Type of the build" required: true type: string +permissions: + contents: read + jobs: get-matrix: runs-on: ubuntu-latest @@ -16,7 +24,7 @@ jobs: outputs: matrix: ${{ steps.matrix.outputs.prop }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: "Get matrix" id: matrix uses: notiz-dev/github-action-json-property@release @@ -38,11 +46,29 @@ jobs: DATABASE_URL: "pgsql://postgres:postgres@127.0.0.1/sylius?charset=utf8&serverVersion=${{ matrix.postgres }}" steps: - - - uses: actions/checkout@v3 + - name: Set variables + shell: bash + env: + BRANCH: ${{ inputs.branch }} + run: | + if [ "$BRANCH" == "1.12" ]; then + echo "USE_LEGACY_POSTGRES_SETUP=yes" >> $GITHUB_ENV + else + echo "USE_LEGACY_POSTGRES_SETUP=no" >> $GITHUB_ENV + fi + + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 - name: Build application - uses: SyliusLabs/BuildTestAppAction@v2.0 + uses: SyliusLabs/BuildTestAppAction@v2.2 with: build_type: "sylius" cache_key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-" @@ -50,6 +76,7 @@ jobs: e2e: "yes" database: "postgresql" database_version: ${{ matrix.postgres }} + legacy_postgresql_setup: ${{ env.USE_LEGACY_POSTGRES_SETUP }} php_version: ${{ matrix.php }} symfony_version: ${{ matrix.symfony }} diff --git a/.github/workflows/ci_e2e-unstable.yaml b/.github/workflows/ci_e2e-unstable.yaml index 54e6718e36..da755d9c47 100644 --- a/.github/workflows/ci_e2e-unstable.yaml +++ b/.github/workflows/ci_e2e-unstable.yaml @@ -10,6 +10,9 @@ on: default: false workflow_dispatch: ~ +permissions: + contents: read + jobs: behat-no-js-unstable: runs-on: ubuntu-latest @@ -29,7 +32,7 @@ jobs: steps: - - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Change minimum-stability to dev run: | @@ -37,7 +40,7 @@ jobs: composer config prefer-stable false - name: Build application - uses: SyliusLabs/BuildTestAppAction@v2.0 + uses: SyliusLabs/BuildTestAppAction@v2.2 with: build_type: "sylius" cache_key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-php-${{ matrix.php }}-symfony-${{ matrix.symfony }}-" diff --git a/.github/workflows/ci_frontend.yaml b/.github/workflows/ci_frontend.yaml index accfe5b347..aafd24246e 100644 --- a/.github/workflows/ci_frontend.yaml +++ b/.github/workflows/ci_frontend.yaml @@ -4,6 +4,11 @@ on: workflow_dispatch: ~ workflow_call: inputs: + branch: + description: "Branch" + required: false + type: string + default: "" type: description: "Type of the build" required: true @@ -16,13 +21,22 @@ jobs: outputs: matrix: ${{ steps.matrix.outputs.prop }} steps: - - uses: actions/checkout@v3 - - name: "Get matrix" + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + uses: actions/checkout@v4 + if: "${{ inputs.branch == '' }}" + - + name: "Get matrix" id: matrix uses: notiz-dev/github-action-json-property@release with: path: '.github/workflows/matrix.json' - prop_path: '${{ inputs.type }}.frontend' + prop_path: '${{ inputs.type }}.e2e-mysql' frontend: needs: get-matrix @@ -37,8 +51,26 @@ jobs: DATABASE_URL: "mysql://root:root@127.0.0.1/sylius?charset=utf8mb4&serverVersion=8.0" steps: - - - uses: actions/checkout@v3 + - name: Set variables + shell: bash + env: + BRANCH: ${{ inputs.branch }} + run: | + if [ "$BRANCH" == "1.12" ]; then + echo "USE_LEGACY_POSTGRES_SETUP=yes" >> $GITHUB_ENV + else + echo "USE_LEGACY_POSTGRES_SETUP=no" >> $GITHUB_ENV + fi + + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 - name: Build application uses: SyliusLabs/BuildTestAppAction@v2.0 @@ -47,5 +79,5 @@ jobs: e2e_js: "yes" database_version: "8.0" php_version: "8.1" - symfony_version: "6.4.*" + symfony_version: "^6.4" node_version: ${{ matrix.node }} diff --git a/.github/workflows/ci_packages-unstable.yaml b/.github/workflows/ci_packages-unstable.yaml index 7b7a09e4e9..67118e7639 100644 --- a/.github/workflows/ci_packages-unstable.yaml +++ b/.github/workflows/ci_packages-unstable.yaml @@ -14,6 +14,9 @@ on: type: boolean default: false +permissions: + contents: read + jobs: get-matrix: runs-on: ubuntu-latest @@ -21,7 +24,7 @@ jobs: outputs: matrix: ${{ steps.matrix.outputs.prop }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: "Get matrix" id: matrix uses: notiz-dev/github-action-json-property@release @@ -34,7 +37,7 @@ jobs: runs-on: ubuntu-latest name: "PHP ${{ matrix.php }}, Symfony ${{ matrix.symfony }} (Unstable Dependencies)" timeout-minutes: 25 - + strategy: fail-fast: false matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }} @@ -46,7 +49,7 @@ jobs: steps: - - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -65,7 +68,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache Composer - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-symfony-${{ matrix.symfony }}-" diff --git a/.github/workflows/ci_packages.yaml b/.github/workflows/ci_packages.yaml index 85beae4b94..8c42f67011 100644 --- a/.github/workflows/ci_packages.yaml +++ b/.github/workflows/ci_packages.yaml @@ -4,11 +4,19 @@ on: workflow_dispatch: ~ workflow_call: inputs: + branch: + description: "Branch" + required: false + type: string + default: "" type: description: "Type of the build" required: true type: string +permissions: + contents: read + jobs: get-matrix: runs-on: ubuntu-latest @@ -16,7 +24,16 @@ jobs: outputs: matrix: ${{ steps.matrix.outputs.prop }} steps: - - uses: actions/checkout@v3 + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 + - name: "Get matrix" id: matrix uses: notiz-dev/github-action-json-property@release @@ -30,7 +47,16 @@ jobs: outputs: matrix: ${{ steps.matrix.outputs.prop }} steps: - - uses: actions/checkout@v3 + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 + - name: "Get matrix" id: matrix uses: notiz-dev/github-action-json-property@release @@ -47,15 +73,22 @@ jobs: strategy: fail-fast: false matrix: ${{ fromJson(needs.get-matrix.outputs.matrix) }} - + env: COMPOSER_ROOT_VERSION: "dev-main" SYMFONY_VERSION: "${{ matrix.symfony }}" UNSTABLE: "no" steps: - - - uses: actions/checkout@v3 + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -75,19 +108,19 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache Composer - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-symfony-${{ matrix.symfony }}-" restore-keys: | "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-symfony-${{ matrix.symfony }}-" - + - name: Install dependencies run: composer update --no-interaction --no-scripts - name: "Run pipeline" run: find src/Sylius -mindepth 3 -maxdepth 3 -type f -name composer.json -exec dirname '{}' \; | sed -e 's/src\/Sylius\///g' | sort | jq --raw-input . | jq --slurp . | jq -c . | xargs -0 vendor/bin/robo ci:packages - + test_with_swiftmailer: needs: get-matrix-swiftmailer runs-on: ubuntu-latest @@ -97,7 +130,7 @@ jobs: strategy: fail-fast: false matrix: ${{fromJson(needs.get-matrix-swiftmailer.outputs.matrix)}} - + env: APP_ENV: "test_with_swiftmailer" COMPOSER_ROOT_VERSION: "dev-main" @@ -107,8 +140,15 @@ jobs: PACKAGES: '["Bundle/CoreBundle", "Bundle/ApiBundle", "Bundle/AdminBundle"]' steps: - - - uses: actions/checkout@v3 + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -122,13 +162,13 @@ jobs: composer global config --no-plugins allow-plugins.symfony/flex true composer global require --no-progress --no-scripts --no-plugins "symfony/flex:^2.4" composer config extra.symfony.require "${{ matrix.symfony }}" - + - name: Get Composer cache directory id: composer-cache run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache Composer - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: "${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-symfony-${{ matrix.symfony }}-" diff --git a/.github/workflows/ci_static-checks.yaml b/.github/workflows/ci_static-checks.yaml index 8d0ac06d7b..78b5b4f185 100644 --- a/.github/workflows/ci_static-checks.yaml +++ b/.github/workflows/ci_static-checks.yaml @@ -4,11 +4,19 @@ on: workflow_dispatch: ~ workflow_call: inputs: + branch: + description: "Branch" + required: false + type: string + default: "" type: description: "Type of the build" required: true type: string +permissions: + contents: read + jobs: get-matrix: runs-on: ubuntu-latest @@ -16,7 +24,16 @@ jobs: outputs: matrix: ${{ steps.matrix.outputs.prop }} steps: - - uses: actions/checkout@v3 + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 + - name: "Get matrix" id: matrix @@ -36,8 +53,15 @@ jobs: APP_ENV: test_cached DATABASE_URL: "mysql://root:root@127.0.0.1/sylius?charset=utf8mb4&serverVersion=8.0" steps: - - - uses: actions/checkout@v3 + - name: "Checkout (With Branch)" + if: "${{ inputs.branch != '' }}" + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: "Checkout" + if: "${{ inputs.branch == '' }}" + uses: actions/checkout@v4 - name: "Setup PHP" uses: shivammathur/setup-php@v2 @@ -64,12 +88,16 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: "Setup cache" - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ${{ steps.composer-cache.outputs.dir }} key: ${{ github.run_id }}-${{ runner.os }}-${{ hashFiles('composer.json') }}-symfony-${{ matrix.symfony }}-api-platform-${{ matrix.api-platform }} + - name: "Require ext-random" + if: matrix.php == '8.2' + run: composer require ext-random --no-update --no-scripts --no-interaction + - name: "Install dependencies" run: composer update --no-interaction --no-scripts diff --git a/.github/workflows/documentation.yaml b/.github/workflows/documentation.yaml index beaa5a1cd6..165360aec3 100644 --- a/.github/workflows/documentation.yaml +++ b/.github/workflows/documentation.yaml @@ -16,6 +16,9 @@ on: cron: "0 1 * * 6" # Run at 1am every Saturday workflow_dispatch: ~ +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest @@ -26,7 +29,7 @@ jobs: steps: - - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install documentation dependencies diff --git a/.github/workflows/matrix.json b/.github/workflows/matrix.json index e7209e920d..600d927e81 100644 --- a/.github/workflows/matrix.json +++ b/.github/workflows/matrix.json @@ -3,12 +3,12 @@ "static-checks": { "include": [ { - "php": "8.0", + "php": "8.1", "symfony": "^5.4", "api-platform": "^2.7.10" }, { - "php": "8.1", + "php": "8.2", "symfony": "^6.4", "api-platform": "^2.7.10" } @@ -17,48 +17,47 @@ "e2e-mariadb": { "include": [ { - "php": "8.0", + "php": "8.1", "symfony": "^5.4", "mariadb": "10.4.10", - "dbal": "^2.7" + "state_machine_adapter": "winzou_state_machine" }, { - "php": "8.1", + "php": "8.2", "symfony": "^6.4", "mariadb": "10.4.10", - "dbal": "^3.0" + "dbal": "^3.0", + "state_machine_adapter": "symfony_workflow" } ] }, "e2e-mysql": { "include": [ { - "php": "8.0", + "php": "8.1", "symfony": "^5.4", "api-platform": "^2.7.10", - "mysql": "5.7", - "twig": "^2.12", - "dbal": "^2.7" + "mysql": "8.0", + "twig": "^2.12" }, { - "php": "8.1", + "php": "8.2", "symfony": "^6.4", "api-platform": "^2.7.10", "mysql": "8.0", - "twig": "^3.3", - "dbal": "^3.0" + "twig": "^3.3" } ] }, "e2e-pgsql": { "include": [ { - "php": "8.0", + "php": "8.1", "symfony": "^5.4", "postgres": "13.9" }, { - "php": "8.1", + "php": "8.2", "symfony": "^6.4", "postgres": "14.6" } @@ -74,11 +73,11 @@ "packages": { "include": [ { - "php": "8.0", + "php": "8.1", "symfony": "^5.4" }, { - "php": "8.1", + "php": "8.2", "symfony": "^6.4" } ] @@ -86,7 +85,7 @@ "packages-swiftmailer": { "include": [ { - "php": "8.0", + "php": "8.1", "symfony": "^5.4" } ] @@ -94,103 +93,39 @@ }, "full": { "static-checks": { - "php": [ "8.0", "8.1" ], + "php": [ "8.1", "8.2" ], "symfony": [ "^5.4", "^6.4" ], - "api-platform": [ "^2.7.10" ], - "exclude": [ - { - "php": "8.0", - "symfony": "^6.4" - } - ] + "api-platform": [ "^2.7.10" ] }, "e2e-mariadb": { - "php": [ "8.0", "8.1" ], + "php": [ "8.1", "8.2" ], "symfony": [ "^5.4", "^6.4" ], "mariadb": [ "10.4.10" ], - "dbal": [ "^2.7", "^3.0" ], - "exclude": [ - { - "php": "8.0", - "symfony": "^6.4" - } - ] + "state_machine_adapter": [ "winzou_state_machine", "symfony_workflow" ] }, "e2e-mysql": { - "php": [ "8.0", "8.1" ], + "php": [ "8.1", "8.2" ], "symfony": [ "^5.4", "^6.4" ], "api-platform": [ "^2.7.10" ], - "mysql": [ "5.7", "8.0" ], + "mysql": [ "8.0" ], "twig": [ "^3.3" ], - "dbal": [ "^3.0" ], "include": [ - { - "php": "8.0", - "symfony": "^5.4", - "api-platform": "^2.7.10", - "mysql": "5.7", - "twig": "^2.12", - "dbal": "^3.0" - }, - { - "php": "8.0", - "symfony": "^5.4", - "api-platform": "^2.7.10", - "mysql": "5.7", - "twig": "^2.12", - "dbal": "^2.7" - }, { "php": "8.1", "symfony": "^5.4", "api-platform": "^2.7.10", "mysql": "8.0", - "twig": "^3.3", - "dbal": "^3.0" - }, - { - "php": "8.1", - "symfony": "~6.3.0", - "api-platform": "^2.7.10", - "mysql": "8.0", - "twig": "^3.3", - "dbal": "^3.0" - }, - { - "php": "8.1", - "symfony": "^6.4", - "api-platform": "^2.7.10", - "mysql": "8.0", - "twig": "^3.3", - "dbal": "^3.0" - } - ], - "exclude": [ - { - "php": "8.0", - "symfony": "^6.4" + "twig": "^2.12" } ] }, "e2e-pgsql": { - "php": [ "8.0", "8.1" ], + "php": [ "8.1", "8.2" ], "symfony": [ "^5.4", "^6.4" ], - "postgres": [ "13.9", "14.6" ], - "exclude": [ - { - "php": "8.0", - "symfony": "^6.4" - } - ] + "postgres": [ "13.9", "14.6" ] }, "frontend": { "include": [ - { - "node": "14.x" - }, - { - "node": "16.x" - }, { "node": "18.x" }, @@ -200,17 +135,11 @@ ] }, "packages": { - "php": [ "8.0", "8.1" ], - "symfony": [ "^5.4", "^6.4" ], - "exclude": [ - { - "php": "8.0", - "symfony": "^6.4" - } - ] + "php": [ "8.1", "8.2" ], + "symfony": [ "^5.4", "^6.4" ] }, "packages-swiftmailer": { - "php": [ "8.0", "8.1" ], + "php": [ "8.1" ], "symfony": [ "^5.4" ] } } diff --git a/.github/workflows/refactor.yaml b/.github/workflows/refactor.yaml index 8ad1693e45..4e65f9c93d 100644 --- a/.github/workflows/refactor.yaml +++ b/.github/workflows/refactor.yaml @@ -3,9 +3,13 @@ name: Refactor on: schedule: - - cron: "0 2 * * MON" # Run at 2am every Monday + cron: "0 2 * * *" # Run every day at 2am workflow_dispatch: ~ +permissions: + contents: write + pull-requests: write + jobs: coding-standard: runs-on: ubuntu-latest @@ -19,11 +23,17 @@ jobs: strategy: fail-fast: false matrix: - branch: ["1.11", "1.12", "1.13"] + include: + - + branch: "1.12" + php: "8.0" + - + branch: "1.13" + php: "8.1" steps: - - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ matrix.branch }} @@ -31,7 +41,7 @@ jobs: name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.0 # the lowest PHP version working with ECS + php-version: ${{ matrix.php }} - name: Install PHP dependencies diff --git a/.github/workflows/social-media-notifications.yaml b/.github/workflows/social-media-notifications.yaml index 0e6f27e157..d551d6e7a1 100644 --- a/.github/workflows/social-media-notifications.yaml +++ b/.github/workflows/social-media-notifications.yaml @@ -4,12 +4,15 @@ on: release: types: [created] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Get current date id: date diff --git a/.github/workflows/upmerge_pr.yaml b/.github/workflows/upmerge_pr.yaml new file mode 100644 index 0000000000..8dc412134f --- /dev/null +++ b/.github/workflows/upmerge_pr.yaml @@ -0,0 +1,73 @@ +name: Upmerge PR + +on: + schedule: + - + cron: "0 2 * * *" + workflow_dispatch: ~ + +permissions: + contents: write + pull-requests: write + +jobs: + upmerge: + runs-on: ubuntu-latest + if: github.repository == 'Sylius/Sylius' + name: "Upmerge PR" + timeout-minutes: 5 + strategy: + fail-fast: false + matrix: + include: + - + base_branch: "1.12" + target_branch: "1.13" + - + base_branch: "1.13" + target_branch: "2.0" + - + base_branch: "2.0" + target_branch: "bootstrap-admin-panel" + + steps: + - + uses: actions/checkout@v4 + with: + ref: ${{ matrix.target_branch }} + + - + name: Reset upmerge branch + run: | + git fetch origin ${{ matrix.base_branch }}:${{ matrix.base_branch }} + git reset --hard ${{ matrix.base_branch }} + + - + name: Create Pull Request + uses: peter-evans/create-pull-request@v4 + with: + token: ${{ secrets.SYLIUS_BOT_PAT }} + title: '[UPMERGE] ${{ matrix.base_branch }} -> ${{ matrix.target_branch }}' + body: | + This PR has been generated automatically. + For more details see [upmerge_pr.yaml](/Sylius/Sylius/blob/1.13/.github/workflows/upmerge_pr.yaml). + + **Remember!** The upmerge should always be merged with using `Merge pull request` button. + + In case of conflicts, please resolve them manually with usign the following commands: + ``` + git fetch upstream + gh pr checkout + git merge upstream/${{ matrix.target_branch }} -m "Resolve conflicts between ${{ matrix.base_branch }} and ${{ matrix.target_branch }}" + ``` + + If you use other name for the upstream remote, please replace `upstream` with the name of your remote pointing to the `Sylius/Sylius` repository. + + Once the conflicts are resolved, please run `git merge --continue` and change the commit title to + ``` + Resolve conflicts between ${{ matrix.base_branch }} and ${{ matrix.target_branch }} + ``` + branch: "upmerge/${{ matrix.base_branch }}_${{ matrix.target_branch }}" + delete-branch: true + branch-suffix: "short-commit-hash" + base: ${{ matrix.target_branch }} diff --git a/.gitignore b/.gitignore index d9be2469e6..20a613140d 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,5 @@ docker-compose.override.yml /public/build/ npm-debug.log yarn-error.log +yarn.lock ###< symfony/webpack-encore-bundle ### diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000000..36dd63f63f --- /dev/null +++ b/.npmignore @@ -0,0 +1 @@ +**/** diff --git a/CHANGELOG-1.13.md b/CHANGELOG-1.13.md new file mode 100644 index 0000000000..f28efb797a --- /dev/null +++ b/CHANGELOG-1.13.md @@ -0,0 +1,371 @@ +# CHANGELOG FOR `1.13.X` + +## v1.13.0-ALPHA.1 (2024-02-05) + +#### Details + +- [#14379](https://github.com/Sylius/Sylius/issues/14379) [Admin] Cart promotions translations for labels ([@ernestWarwas](https://github.com/ernestWarwas)) +- [#14483](https://github.com/Sylius/Sylius/issues/14483) Updating the RemoveExpiredCartCommand ([@mamazu](https://github.com/mamazu)) +- [#14502](https://github.com/Sylius/Sylius/issues/14502) [Maintenance] Improve deprecation notice ([@lchrusciel](https://github.com/lchrusciel)) +- [#14519](https://github.com/Sylius/Sylius/issues/14519) Refresh readme banner ([@kulczy](https://github.com/kulczy)) +- [#14478](https://github.com/Sylius/Sylius/issues/14478) Add PaymentFixture ([@TheMilek](https://github.com/TheMilek)) +- [#14550](https://github.com/Sylius/Sylius/issues/14550) [Admin][Customer] Filtering customers by groups ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14565](https://github.com/Sylius/Sylius/issues/14565) [Order] Add cart summary event ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14485](https://github.com/Sylius/Sylius/issues/14485) Adding a filter for state in product review grid ([@mamazu](https://github.com/mamazu)) +- [#14529](https://github.com/Sylius/Sylius/issues/14529) Prepare for releasing @sylius-ui/frontend npm package ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14607](https://github.com/Sylius/Sylius/issues/14607) [CS][DX] Refactor +- [#14571](https://github.com/Sylius/Sylius/issues/14571) Create admin user with CLI ([@johbuch](https://github.com/johbuch), [@ernestWarwas](https://github.com/ernestWarwas), [@Rafikooo](https://github.com/Rafikooo)) +- [#14568](https://github.com/Sylius/Sylius/issues/14568) [Catalog Promotions] Put catalog promotions into the processing state right after the delete request and keep this state until being removed ([@coldic3](https://github.com/coldic3)) +- [#14660](https://github.com/Sylius/Sylius/issues/14660) [Maintenance] PHPUnit upgrade to ^9.5 ([@Rafikooo](https://github.com/Rafikooo)) +- [#14654](https://github.com/Sylius/Sylius/issues/14654) Add a cookbook about dealing with multiple channels in console commands ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14629](https://github.com/Sylius/Sylius/issues/14629) [DX] Store passwords in plaintext in test environment ([@coldic3](https://github.com/coldic3)) +- [#14658](https://github.com/Sylius/Sylius/issues/14658) [CS][DX] Refactor ([@bot](https://github.com/bot)@[@sylius](https://github.com/sylius).[@org](https://github.com/org)) +- [#14668](https://github.com/Sylius/Sylius/issues/14668) Improve "Handle multiple channels in CLI" cookbook ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14696](https://github.com/Sylius/Sylius/issues/14696) Remove yarn.lock file ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14708](https://github.com/Sylius/Sylius/issues/14708) Add missing final keywords to Spec tests ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14709](https://github.com/Sylius/Sylius/issues/14709) Drop unneeded @javascript tag ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14718](https://github.com/Sylius/Sylius/issues/14718) [CS][DX] Refactor +- [#14598](https://github.com/Sylius/Sylius/issues/14598) [API] Mapping/serialization cleanup ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14655](https://github.com/Sylius/Sylius/issues/14655) Improve error handling while password resetting ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14713](https://github.com/Sylius/Sylius/issues/14713) [Core] Optimize UnpaidOrdersStateUpdater ([@coldic3](https://github.com/coldic3)) +- [#14693](https://github.com/Sylius/Sylius/issues/14693) [Feature] Allows `row_attr` on form rows ([@Prometee](https://github.com/Prometee)) +- [#14724](https://github.com/Sylius/Sylius/issues/14724) As an Admin, I want to modify taxons of a product ([@everwhatever](https://github.com/everwhatever)) +- [#14710](https://github.com/Sylius/Sylius/issues/14710) Refactor CI ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14732](https://github.com/Sylius/Sylius/issues/14732) [Fix] Mark PHPSpec class as final ([@Rafikooo](https://github.com/Rafikooo)) +- [#13445](https://github.com/Sylius/Sylius/issues/13445) [Promotion] Add a label for filters ([@pjurasek](https://github.com/pjurasek)) +- [#14581](https://github.com/Sylius/Sylius/issues/14581) Fix dev dockerfile: add curl ([@Nek-](https://github.com/Nek-)) +- [#14734](https://github.com/Sylius/Sylius/issues/14734) Adjust GitHub Actions jobs' names ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14741](https://github.com/Sylius/Sylius/issues/14741) [CS][DX] Refactor +- [#14698](https://github.com/Sylius/Sylius/issues/14698) ApiBundle fixes in composer.js ([@dawkaa](https://github.com/dawkaa)) +- [#14742](https://github.com/Sylius/Sylius/issues/14742) Refactor Workflows to use two type of builds ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14747](https://github.com/Sylius/Sylius/issues/14747) Make build notifier running always event on failed builds ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14665](https://github.com/Sylius/Sylius/issues/14665) [Feature][Command] CreateAdminUserCommand - add AdminUser entity validation ([@Rafikooo](https://github.com/Rafikooo)) +- [#14737](https://github.com/Sylius/Sylius/issues/14737) Add a custom workflow for Symfony 6.3 ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14470](https://github.com/Sylius/Sylius/issues/14470) Update doctrine/cache requirement from ^1.10 to ^2.2 ([@dependabot](https://github.com/dependabot)[[@bot](https://github.com/bot)]) +- [#14771](https://github.com/Sylius/Sylius/issues/14771) Change the scheduled hour for full workflow to be different than in 1.12 branch ([@GSadee](https://github.com/GSadee)) +- [#14769](https://github.com/Sylius/Sylius/issues/14769) [CS][DX] Refactor +- [#14786](https://github.com/Sylius/Sylius/issues/14786) Fix CI Full to run both 1.12 and 1.13 ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14756](https://github.com/Sylius/Sylius/issues/14756) [API] Get admin detail information ([@dawkaa](https://github.com/dawkaa)) +- [#14754](https://github.com/Sylius/Sylius/issues/14754) Add PHP 8.2 to workflows ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14728](https://github.com/Sylius/Sylius/issues/14728) [API] Fix admin's side can't create new taxonomy ([@dawkaa](https://github.com/dawkaa)) +- [#12556](https://github.com/Sylius/Sylius/issues/12556) Add an option to use icon-only buttons on grids ([@loic425](https://github.com/loic425)) +- [#13440](https://github.com/Sylius/Sylius/issues/13440) Add documentation for non-labeled actions ([@loic425](https://github.com/loic425)) +- [#14790](https://github.com/Sylius/Sylius/issues/14790) Update RequestBuilder.php to PHP 8 new features ([@dawkaa](https://github.com/dawkaa)) +- [#14794](https://github.com/Sylius/Sylius/issues/14794) [CS][DX] Refactor +- [#13045](https://github.com/Sylius/Sylius/issues/13045) Association hydrator private in product repo should be protected ([@Nek-](https://github.com/Nek-)) +- [#14821](https://github.com/Sylius/Sylius/issues/14821) [CS][DX] Refactor +- [#14822](https://github.com/Sylius/Sylius/issues/14822) [CI] Remove PHP 8.2 from packages tests of the unmaintained Swiftmailer ([@GSadee](https://github.com/GSadee)) +- [#14798](https://github.com/Sylius/Sylius/issues/14798) Reorder order's sidebar in admin ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14811](https://github.com/Sylius/Sylius/issues/14811) Cover managing the tax rates in API ([@hatem20](https://github.com/hatem20), [@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14831](https://github.com/Sylius/Sylius/issues/14831) [CS][DX] Refactor +- [#14833](https://github.com/Sylius/Sylius/issues/14833) Allow to define a priority on autoconfigured order processors and cart contexts ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14840](https://github.com/Sylius/Sylius/issues/14840) [CS][DX] Refactor +- [#14843](https://github.com/Sylius/Sylius/issues/14843) Add a note about autoconfiguring order processor with an attribute ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14623](https://github.com/Sylius/Sylius/issues/14623) chore: tag OrderTaxesApplicatorInterface ([@Florian-Merle](https://github.com/Florian-Merle)) +- [#14848](https://github.com/Sylius/Sylius/issues/14848) [Dependencies] Bump up API Platform to 2.7.10 ([@GSadee](https://github.com/GSadee)) +- [#10690](https://github.com/Sylius/Sylius/issues/10690) [Attributes] Add FloatAttributeType Feature ([@panigrc](https://github.com/panigrc), [@TheMilek](https://github.com/TheMilek)) +- [#14852](https://github.com/Sylius/Sylius/issues/14852) [PriceHistory] Behat scenarios ([@Rafikooo](https://github.com/Rafikooo)) +- [#14860](https://github.com/Sylius/Sylius/issues/14860) [Behat][API] Common saving within contexts ([@Rafikooo](https://github.com/Rafikooo)) +- [#14864](https://github.com/Sylius/Sylius/issues/14864) [TaxRate][UI] Add missing behat step implementation ([@Rafikooo](https://github.com/Rafikooo)) +- [#14862](https://github.com/Sylius/Sylius/issues/14862) [Behat][PriceHistory] Adjust scenarios for displaying lowest price according to the excluded taxons ([@GSadee](https://github.com/GSadee)) +- [#14854](https://github.com/Sylius/Sylius/issues/14854) [PriceHistory][API] ChannelPricingLogEntry implementation ([@Rafikooo](https://github.com/Rafikooo)) +- [#14866](https://github.com/Sylius/Sylius/issues/14866) [PriceHistory][UI] ChannelPricingLogEntry implementation ([@Rafikooo](https://github.com/Rafikooo)) +- [#14867](https://github.com/Sylius/Sylius/issues/14867) [Attributes] FloatType input type as a number instead of text ([@TheMilek](https://github.com/TheMilek)) +- [#14846](https://github.com/Sylius/Sylius/issues/14846) [Shipment] Reduce amount of queries during shipping eligibility checking ([@lchrusciel](https://github.com/lchrusciel)) +- [#12781](https://github.com/Sylius/Sylius/issues/12781) Add "enabled" property to API serialization ([@Nek-](https://github.com/Nek-)) +- [#14676](https://github.com/Sylius/Sylius/issues/14676) Add EmailMessagesProvider ([@TheMilek](https://github.com/TheMilek)) +- [#14890](https://github.com/Sylius/Sylius/issues/14890) [CS][DX] Refactor +- [#14872](https://github.com/Sylius/Sylius/issues/14872) Fix dependency security vulerability for enshrined/svg-sanitize package ([@cosminsandu](https://github.com/cosminsandu)) +- [#14873](https://github.com/Sylius/Sylius/issues/14873) Improve displaying info about insufficient stock in the cart ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14882](https://github.com/Sylius/Sylius/issues/14882) [PriceHistory] Display of information about the lowest price before the discount & channel setup ([@Rafikooo](https://github.com/Rafikooo)) +- [#14875](https://github.com/Sylius/Sylius/issues/14875) [PriceHistory] Add removing price history command feature ([@TheMilek](https://github.com/TheMilek)) +- [#14893](https://github.com/Sylius/Sylius/issues/14893) [OAuth] Up access and refresh tokens' length ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14894](https://github.com/Sylius/Sylius/issues/14894) Avoid getting the theme if we have it already ([@jacquesbh](https://github.com/jacquesbh), [@NoResponseMate](https://github.com/NoResponseMate)) +- [#14899](https://github.com/Sylius/Sylius/issues/14899) [PriceHistory] Extract config ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14903](https://github.com/Sylius/Sylius/issues/14903) [PriceHistory] Combine migrations into one ([@TheMilek](https://github.com/TheMilek)) +- [#14904](https://github.com/Sylius/Sylius/issues/14904) [PriceHistory] Asynchronous lowest price processing ([@Rafikooo](https://github.com/Rafikooo)) +- [#14909](https://github.com/Sylius/Sylius/issues/14909) [CS][DX] Refactor ([@github-actions](https://github.com/github-actions)[[@bot](https://github.com/bot)]) +- [#14915](https://github.com/Sylius/Sylius/issues/14915) [Maintenance] Rename OnFlushEntityObserverListener service ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14916](https://github.com/Sylius/Sylius/issues/14916) [API][ProductTaxon] Add unique combination validation ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14900](https://github.com/Sylius/Sylius/issues/14900) [UpgradeFile] Introduce upgrade from Sylius 1.12 with PriceHistoryPlugin to Sylius 1.13 ([@TheMilek](https://github.com/TheMilek)) +- [#14911](https://github.com/Sylius/Sylius/issues/14911) [PriceHistory] Add Compiler Pass for upgrade between Sylius 1.12 and Sylius 1.13 version ([@TheMilek](https://github.com/TheMilek)) +- [#14928](https://github.com/Sylius/Sylius/issues/14928) [Maintenance] Add missing Spec for HasEnabledEntityValidator ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14837](https://github.com/Sylius/Sylius/issues/14837) [ProductVariant][API] Resource improvements ([@Rafikooo](https://github.com/Rafikooo)) +- [#14930](https://github.com/Sylius/Sylius/issues/14930) [Maintenance] Fix schema update with longer UserOAuth tokens ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14931](https://github.com/Sylius/Sylius/issues/14931) [Behat][API] Checking out as guest with a registered email ([@coldic3](https://github.com/coldic3)) +- [#14934](https://github.com/Sylius/Sylius/issues/14934) Cover accessing non-existing product via API scenario ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14933](https://github.com/Sylius/Sylius/issues/14933) Allow to filter products via API only by enabled taxon ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14943](https://github.com/Sylius/Sylius/issues/14943) Cover handling different currencies on multiple channels via API scenarios ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14939](https://github.com/Sylius/Sylius/issues/14939) [Behat][API] Add "no-api" tag to the changing payment method page scenario ([@coldic3](https://github.com/coldic3)) +- [#14937](https://github.com/Sylius/Sylius/issues/14937) Add calculating OrderItemsSubtotal to the Order model ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14936](https://github.com/Sylius/Sylius/issues/14936) Cover applying correct taxes for shipping scenarios ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14922](https://github.com/Sylius/Sylius/issues/14922) [CS][DX] Refactor ([@github-actions](https://github.com/github-actions)[[@bot](https://github.com/bot)]) +- [#14942](https://github.com/Sylius/Sylius/issues/14942) Cover applying correct taxes for shipping with tax rate included in price scenarios ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14947](https://github.com/Sylius/Sylius/issues/14947) Cover reapplying promotion on cart changes scenarios in API ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14921](https://github.com/Sylius/Sylius/issues/14921) [Fixtures] add possibility to configure shipping_address_in_checkout_required ([@BastienGoze](https://github.com/BastienGoze)) +- [#14924](https://github.com/Sylius/Sylius/issues/14924) Update sylius/calendar requirement from ^0.3 || ^0.4 to ^0.5.0 ([@dependabot](https://github.com/dependabot)[[@bot](https://github.com/bot)], [@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14951](https://github.com/Sylius/Sylius/issues/14951) Cover Api steps in paying offline during checkout scenario ([@TheMilek](https://github.com/TheMilek)) +- [#14960](https://github.com/Sylius/Sylius/issues/14960) Cover Api steps in paying offline during checkout as guest scenario ([@TheMilek](https://github.com/TheMilek)) +- [#14959](https://github.com/Sylius/Sylius/issues/14959) [Behat][API] Retrying the payment with different Offline payment ([@coldic3](https://github.com/coldic3)) +- [#14945](https://github.com/Sylius/Sylius/issues/14945) Add @no-api to redirecting_to_products_when_there_is_a_trailing_slash_in_path scenarios ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14948](https://github.com/Sylius/Sylius/issues/14948) Cover reverting previously applied discount on cart scenarios in API ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14885](https://github.com/Sylius/Sylius/issues/14885) [DX] Ease adding more data to variant options map ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14952](https://github.com/Sylius/Sylius/issues/14952) [DX] Improve ChannelCollector customizability and performance ([@coldic3](https://github.com/coldic3)) +- [#14950](https://github.com/Sylius/Sylius/issues/14950) [Migrations] Support PostgreSQL migrations ([@Rafikooo](https://github.com/Rafikooo)) +- [#14791](https://github.com/Sylius/Sylius/issues/14791) Add a taxon delete section ([@dawkaa](https://github.com/dawkaa)) +- [#14964](https://github.com/Sylius/Sylius/issues/14964) Clean up conflicts for sylius/sylius ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#14989](https://github.com/Sylius/Sylius/issues/14989) [ProductTaxon][Product] Improve validation ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14975](https://github.com/Sylius/Sylius/issues/14975) [CS][DX] Refactor ([@github-actions](https://github.com/github-actions)[[@bot](https://github.com/bot)]) +- [#14999](https://github.com/Sylius/Sylius/issues/14999) [CI] Change outdated GitHub action ([@Rafikooo](https://github.com/Rafikooo)) +- [#14969](https://github.com/Sylius/Sylius/issues/14969) [DX] Removed obsolete templating helper ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14919](https://github.com/Sylius/Sylius/issues/14919) Use Symfony HTTP Client in place of Guzzle 6 by default and provide Guzzle 7 support ([@coldic3](https://github.com/coldic3)) +- [#14584](https://github.com/Sylius/Sylius/issues/14584) Fix product review validation's notInRangeMessage ([@diimpp](https://github.com/diimpp)) +- [#15004](https://github.com/Sylius/Sylius/issues/15004) [CS][DX] Refactor ([@github-actions](https://github.com/github-actions)[[@bot](https://github.com/bot)]) +- [#15003](https://github.com/Sylius/Sylius/issues/15003) Cover seeing default shipping method scenarios in API ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15002](https://github.com/Sylius/Sylius/issues/15002) Cover product integrity scenarios in API ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15001](https://github.com/Sylius/Sylius/issues/15001) Cover editing customer profile scenarios in API ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15020](https://github.com/Sylius/Sylius/issues/15020) Cover Viewing only enabled taxons in taxon menu scenarios ([@TheMilek](https://github.com/TheMilek)) +- [#15023](https://github.com/Sylius/Sylius/issues/15023) [Api][Maintenance] Remove ChannelContext from ProductVariantNormalizer ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15009](https://github.com/Sylius/Sylius/issues/15009) Replace `payum/payum` dependency with `payum/core` ([@rimas-kudelis](https://github.com/rimas-kudelis)) +- [#15019](https://github.com/Sylius/Sylius/issues/15019) Update rector/rector requirement from ^0.15.13 to ^0.16.0 ([@dependabot](https://github.com/dependabot)[[@bot](https://github.com/bot)]) +- [#14994](https://github.com/Sylius/Sylius/issues/14994) [Products][Taxon] Allowing deleting a taxon that is a main taxon of a product ([@Rafikooo](https://github.com/Rafikooo)) +- [#15012](https://github.com/Sylius/Sylius/issues/15012) Update guzzlehttp/psr7 requirement from ^1.8 to ^2.5 ([@dependabot](https://github.com/dependabot)[[@bot](https://github.com/bot)]) +- [#14968](https://github.com/Sylius/Sylius/issues/14968) Bump Psalm to v5 ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15036](https://github.com/Sylius/Sylius/issues/15036) Fix CI ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15035](https://github.com/Sylius/Sylius/issues/15035) Remove 1.11 from the refactor workflow ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15039](https://github.com/Sylius/Sylius/issues/15039) Fix CI ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15044](https://github.com/Sylius/Sylius/issues/15044) Add API tag to viewing products with disabled variants scenarios ([@TheMilek](https://github.com/TheMilek)) +- [#15015](https://github.com/Sylius/Sylius/issues/15015) [API] Product variants with options ([@Rafikooo](https://github.com/Rafikooo)) +- [#15046](https://github.com/Sylius/Sylius/issues/15046) [Maintenance] Removed autogenerated migration comments ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14480](https://github.com/Sylius/Sylius/issues/14480) Missing length validation for label in cart promotion translations ([@ernestWarwas](https://github.com/ernestWarwas)) +- [#15045](https://github.com/Sylius/Sylius/issues/15045) Uncomment editing customer profile steps ([@TheMilek](https://github.com/TheMilek)) +- [#14944](https://github.com/Sylius/Sylius/issues/14944) Cover inform customer about order total changes scenarios in API ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15042](https://github.com/Sylius/Sylius/issues/15042) View only enabled taxons ([@TheMilek](https://github.com/TheMilek)) +- [#15028](https://github.com/Sylius/Sylius/issues/15028) Cover viewing product associations via API ([@TheMilek](https://github.com/TheMilek)) +- [#15054](https://github.com/Sylius/Sylius/issues/15054) Cover viewing different price for different product variants API scenario ([@TheMilek](https://github.com/TheMilek)) +- [#15047](https://github.com/Sylius/Sylius/issues/15047) Parametrize operations allowed to bypass filtering carts ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15043](https://github.com/Sylius/Sylius/issues/15043) [Maintenance] Add a note about PostgreSQL migrations support ([@Rafikooo](https://github.com/Rafikooo)) +- [#15060](https://github.com/Sylius/Sylius/issues/15060) [Product] Add an alias to `ProductVariantResolverInterface` ([@coldic3](https://github.com/coldic3)) +- [#15070](https://github.com/Sylius/Sylius/issues/15070) Upmerge 1.12 into 1.13 ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15066](https://github.com/Sylius/Sylius/issues/15066) [CS][DX] Refactor +- [#15083](https://github.com/Sylius/Sylius/issues/15083) Remove deprecated EventSubscriberInterface ([@TheMilek](https://github.com/TheMilek)) +- [#15063](https://github.com/Sylius/Sylius/issues/15063) [API] Allow overriding xml resource mapping ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#14990](https://github.com/Sylius/Sylius/issues/14990) [API] Sorting user orders by order number ([@dawkaa](https://github.com/dawkaa)) +- [#15096](https://github.com/Sylius/Sylius/issues/15096) [Fix] Remove redundant conflict entries ([@Rafikooo](https://github.com/Rafikooo)) +- [#15071](https://github.com/Sylius/Sylius/issues/15071) Update vimeo/psalm requirement from 5.9.* to 5.12.* ([@dependabot](https://github.com/dependabot)[[@bot](https://github.com/bot)]) +- [#15091](https://github.com/Sylius/Sylius/issues/15091) [CS][DX] Refactor ([@github-actions](https://github.com/github-actions)[[@bot](https://github.com/bot)]) +- [#15095](https://github.com/Sylius/Sylius/issues/15095) Viewing children taxons API scenario ceverage ([@TheMilek](https://github.com/TheMilek)) +- [#15103](https://github.com/Sylius/Sylius/issues/15103) Seeing order locale API scenario coverage ([@TheMilek](https://github.com/TheMilek)) +- [#15098](https://github.com/Sylius/Sylius/issues/15098) Add @no-api tag to preventing to pay for cancelled order scenario ([@TheMilek](https://github.com/TheMilek)) +- [#15068](https://github.com/Sylius/Sylius/issues/15068) View only available associations for product ([@TheMilek](https://github.com/TheMilek)) +- [#15092](https://github.com/Sylius/Sylius/issues/15092) Cover Seeing payment method instructions behat API scenario ([@TheMilek](https://github.com/TheMilek)) +- [#15104](https://github.com/Sylius/Sylius/issues/15104) Fixes after reviews ([@TheMilek](https://github.com/TheMilek)) +- [#15085](https://github.com/Sylius/Sylius/issues/15085) [API][Products] Viewing diagonal variants options ([@Rafikooo](https://github.com/Rafikooo)) +- [#15105](https://github.com/Sylius/Sylius/issues/15105) [Maintenance] Update the remaining copyright blocks ([@Rafikooo](https://github.com/Rafikooo)) +- [#15106](https://github.com/Sylius/Sylius/issues/15106) [Maintenance] Update the remaining copyright blocks ([@Rafikooo](https://github.com/Rafikooo)) +- [#15109](https://github.com/Sylius/Sylius/issues/15109) Add workflow for automatic upmerge PR creating ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15113](https://github.com/Sylius/Sylius/issues/15113) Fix auto upmerge workflow ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15112](https://github.com/Sylius/Sylius/issues/15112) [Scenarios] Add @no-api tag for `Addressing an order and sign in scenario` ([@Rafikooo](https://github.com/Rafikooo)) +- [#15087](https://github.com/Sylius/Sylius/issues/15087) [Maintenance][API] Fix ChangedItemQuantityInCart validator alias ([@NoResponseMate](https://github.com/NoResponseMate), [@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15084](https://github.com/Sylius/Sylius/issues/15084) Update Sylius 1.12 with PriceHistoryPlugin to Sylius 1.13 upgrade file ([@TheMilek](https://github.com/TheMilek)) +- [#15061](https://github.com/Sylius/Sylius/issues/15061) Performance improvement of SingleChannelContext ([@coldic3](https://github.com/coldic3)) +- [#15099](https://github.com/Sylius/Sylius/issues/15099) [Product] Composite variant resolver ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15121](https://github.com/Sylius/Sylius/issues/15121) Fix CI ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15122](https://github.com/Sylius/Sylius/issues/15122) [API] Cover signing in validation ([@Rafikooo](https://github.com/Rafikooo)) +- [#15026](https://github.com/Sylius/Sylius/issues/15026) Allow removing locales ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15135](https://github.com/Sylius/Sylius/issues/15135) Revert "Add support for Bunnyshell (#15134)" ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15108](https://github.com/Sylius/Sylius/issues/15108) Prevent changing payment method of cancelled order ([@TheMilek](https://github.com/TheMilek)) +- [#15132](https://github.com/Sylius/Sylius/issues/15132) Add joins on the orders list query builder conditionally ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15072](https://github.com/Sylius/Sylius/issues/15072) [API] Validate endpoints that uses commands ([@Rafikooo](https://github.com/Rafikooo)) +- [#15138](https://github.com/Sylius/Sylius/issues/15138) [Bunnyshell] Adjust the workflows for Fork PRs ([@aris-bunnyshell](https://github.com/aris-bunnyshell)) +- [#15143](https://github.com/Sylius/Sylius/issues/15143) Upmerge adjustments ([@Rafikooo](https://github.com/Rafikooo)) +- [#15111](https://github.com/Sylius/Sylius/issues/15111) [API] Allow command IRI denormalization in arrays ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15158](https://github.com/Sylius/Sylius/issues/15158) [API] Replace Swagger with OpenAPI ([@coldic3](https://github.com/coldic3)) +- [#15188](https://github.com/Sylius/Sylius/issues/15188) [CS][DX] Refactor ([@github-actions](https://github.com/github-actions)[[@bot](https://github.com/bot)]) +- [#15190](https://github.com/Sylius/Sylius/issues/15190) Update vimeo/psalm requirement from 5.13.* to 5.14.* ([@dependabot](https://github.com/dependabot)[[@bot](https://github.com/bot)]) +- [#15174](https://github.com/Sylius/Sylius/issues/15174) Update sonata-project/block-bundle requirement from ^4.2 to ^5.0 ([@dependabot](https://github.com/dependabot)[[@bot](https://github.com/bot)]) +- [#15209](https://github.com/Sylius/Sylius/issues/15209) [Maintenance] Trigger deprecations in OrderPaymentProcessor ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15208](https://github.com/Sylius/Sylius/issues/15208) [Maintenance] Trigger deprecations in taxes applicators ([@GSadee](https://github.com/GSadee)) +- [#15221](https://github.com/Sylius/Sylius/issues/15221) Add explicit mapping between product and reviews, and between product and attributes ([@GSadee](https://github.com/GSadee)) +- [#15220](https://github.com/Sylius/Sylius/issues/15220) [Taxonomy][Form] Taxon slug generator including parent slug also in update mode ([@nicolalazzaro](https://github.com/nicolalazzaro), [@NoResponseMate](https://github.com/NoResponseMate)) +- [#15235](https://github.com/Sylius/Sylius/issues/15235) [Maintenance] Note deprecation in SelectAttributeChoicesCollectionType ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15244](https://github.com/Sylius/Sylius/issues/15244) Refresh README.md ([@kulczy](https://github.com/kulczy)) +- [#15056](https://github.com/Sylius/Sylius/issues/15056) Improve registration workflow ([@fabianaromagnoli](https://github.com/fabianaromagnoli), [@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15254](https://github.com/Sylius/Sylius/issues/15254) Deprecate unused ProductOptionChoiceType ([@TheMilek](https://github.com/TheMilek)) +- [#15259](https://github.com/Sylius/Sylius/issues/15259) [API][Admin] Taxons management ([@GSadee](https://github.com/GSadee)) +- [#15292](https://github.com/Sylius/Sylius/issues/15292) Provide autoconfiguration with attributes for ApiBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15297](https://github.com/Sylius/Sylius/issues/15297) Provide autoconfiguration with attributes for PaymentBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15301](https://github.com/Sylius/Sylius/issues/15301) Provide autoconfiguration with attributes for PayumBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15293](https://github.com/Sylius/Sylius/issues/15293) Provide autoconfiguration with attributes for CoreBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15295](https://github.com/Sylius/Sylius/issues/15295) Provide autoconfiguration with attributes for LocaleBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15294](https://github.com/Sylius/Sylius/issues/15294) Provide autoconfiguration with attributes for CurrencyBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15296](https://github.com/Sylius/Sylius/issues/15296) Improve autoconfiguration with attributes for OrderBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15298](https://github.com/Sylius/Sylius/issues/15298) Improve autoconfiguration with attributes for ProductBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15304](https://github.com/Sylius/Sylius/issues/15304) Provide autoconfiguration with attributes for TaxationBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15303](https://github.com/Sylius/Sylius/issues/15303) Provide autoconfiguration with attributes for ShippingBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15302](https://github.com/Sylius/Sylius/issues/15302) Provide autoconfiguration with attributes for PromotionBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15299](https://github.com/Sylius/Sylius/issues/15299) Provide autoconfiguration with attributes for AttributeBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15300](https://github.com/Sylius/Sylius/issues/15300) Provide autoconfiguration with attributes for ChannelBundle ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15305](https://github.com/Sylius/Sylius/issues/15305) Add missing admin:taxon:update group to TaxonTranslation's locale field ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15307](https://github.com/Sylius/Sylius/issues/15307) Add a deprecation for the sylius_admin_ajax_taxon_move route ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15281](https://github.com/Sylius/Sylius/issues/15281) Only show "View More" button when there are accepted reviews ([@stefandoorn](https://github.com/stefandoorn), [@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15278](https://github.com/Sylius/Sylius/issues/15278) Deprecate ContainsProductRuleUpdater and TotalOfItemsFromTaxonRuleUpdater ([@TheMilek](https://github.com/TheMilek)) +- [#15309](https://github.com/Sylius/Sylius/issues/15309) Update the banner in the README.md ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15317](https://github.com/Sylius/Sylius/issues/15317) [doc] plugin naming - new plugin structure ([@oallain](https://github.com/oallain)) +- [#15322](https://github.com/Sylius/Sylius/issues/15322) Add optional message to API cannot be removed exceptions ([@Wojdylak](https://github.com/Wojdylak)) +- [#15323](https://github.com/Sylius/Sylius/issues/15323) [Maintenance] Change `trigger_error` to `trigger_deprecation` ([@Rafikooo](https://github.com/Rafikooo)) +- [#15324](https://github.com/Sylius/Sylius/issues/15324) Prevent from removing taxon that is in use by a promotion rule ([@TheMilek](https://github.com/TheMilek)) +- [#15315](https://github.com/Sylius/Sylius/issues/15315) Provide the State Machine abstraction ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15335](https://github.com/Sylius/Sylius/issues/15335) Minor typo fix in deprecation message in ZoneChoiceType ([@GSadee](https://github.com/GSadee)) +- [#15333](https://github.com/Sylius/Sylius/issues/15333) [CoreBundle] Add missed deprecation block ([@Rafikooo](https://github.com/Rafikooo)) +- [#15346](https://github.com/Sylius/Sylius/issues/15346) [Maintenance] Deprecate `OrderItemsSubtotalCalculatorInterface` ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15351](https://github.com/Sylius/Sylius/issues/15351) [AdminBundle] Corrected deprecation messages ([@Rafikooo](https://github.com/Rafikooo)) +- [#15338](https://github.com/Sylius/Sylius/issues/15338) [CoreBundle] Move the PaymentFixture class in a proper namespace and deprecate other code ([@Rafikooo](https://github.com/Rafikooo)) +- [#15288](https://github.com/Sylius/Sylius/issues/15288) [Api][Admin] Product Attributes Management ([@NoResponseMate](https://github.com/NoResponseMate), [@GSadee](https://github.com/GSadee)) +- [#15366](https://github.com/Sylius/Sylius/issues/15366) [Api][Attribute] Add types enum to product attribute schema ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15275](https://github.com/Sylius/Sylius/issues/15275) Optimize matching zones to a given address ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15373](https://github.com/Sylius/Sylius/issues/15373) [Api] Mark bulk delete scenarios as @no-api ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15362](https://github.com/Sylius/Sylius/issues/15362) Add OrderShipping Symfony Workflow configuration ([@TheMilek](https://github.com/TheMilek)) +- [#15376](https://github.com/Sylius/Sylius/issues/15376) Component core deprecate order interface methods ([@Wojdylak](https://github.com/Wojdylak)) +- [#15262](https://github.com/Sylius/Sylius/issues/15262) [Maintenance] Deprecate using `parentId` in TaxonSlugController ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15380](https://github.com/Sylius/Sylius/issues/15380) [ShippingBundle] Change deprecated Calendar class ([@Rafikooo](https://github.com/Rafikooo)) +- [#15342](https://github.com/Sylius/Sylius/issues/15342) Add newline to align with other public method ([@stefandoorn](https://github.com/stefandoorn)) +- [#15383](https://github.com/Sylius/Sylius/issues/15383) Fixes to order shipping workflow after review ([@TheMilek](https://github.com/TheMilek)) +- [#15395](https://github.com/Sylius/Sylius/issues/15395) Fix failing phpspec scenario on Symfony 6.3.5 and above ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15397](https://github.com/Sylius/Sylius/issues/15397) Fix failing phpspec scenario on Symfony 6.3.5 and above ([@Wojdylak](https://github.com/Wojdylak)) +- [#15406](https://github.com/Sylius/Sylius/issues/15406) Add Symfony workflow configuration for Shipment ([@TheMilek](https://github.com/TheMilek)) +- [#15407](https://github.com/Sylius/Sylius/issues/15407) [API][Maintenance] Remove usage of deprecated IriConverter ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15422](https://github.com/Sylius/Sylius/issues/15422) [PaymentBundle] Rename void to unknown in sylius_payment state machine ([@Wojdylak](https://github.com/Wojdylak)) +- [#15415](https://github.com/Sylius/Sylius/issues/15415) Add order payment workflow ([@Wojdylak](https://github.com/Wojdylak)) +- [#15425](https://github.com/Sylius/Sylius/issues/15425) [PostgreSQL] Sylius installation fix ([@Rafikooo](https://github.com/Rafikooo)) +- [#15423](https://github.com/Sylius/Sylius/issues/15423) [PaymentBundle] Add new state to sylius payment state machine ([@Wojdylak](https://github.com/Wojdylak)) +- [#15411](https://github.com/Sylius/Sylius/issues/15411) [Workflow] Add payment state machine config ([@NoResponseMate](https://github.com/NoResponseMate), [@Wojdylak](https://github.com/Wojdylak)) +- [#15408](https://github.com/Sylius/Sylius/issues/15408) [Documentation]Updated links that were redirecting to 404 ([@benbd5](https://github.com/benbd5)) +- [#15427](https://github.com/Sylius/Sylius/issues/15427) [PostgreSQL] Sylius installation fix amend ([@Rafikooo](https://github.com/Rafikooo)) +- [#13902](https://github.com/Sylius/Sylius/issues/13902) [API] Reset password : add validations to produce 422 instead of 500 status code ([@Prometee](https://github.com/Prometee), [@Wojdylak](https://github.com/Wojdylak)) +- [#15429](https://github.com/Sylius/Sylius/issues/15429) [Maintenance][Core] Update payment state machine ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15399](https://github.com/Sylius/Sylius/issues/15399) [API][Admin] Managing products ([@GSadee](https://github.com/GSadee)) +- [#15460](https://github.com/Sylius/Sylius/issues/15460) [API][Admin] Managing product variants ([@GSadee](https://github.com/GSadee)) +- [#15457](https://github.com/Sylius/Sylius/issues/15457) [API] Add Customer resources ([@Wojdylak](https://github.com/Wojdylak)) +- [#15466](https://github.com/Sylius/Sylius/issues/15466) Add symfony workflow configuration for order ([@TheMilek](https://github.com/TheMilek)) +- [#15452](https://github.com/Sylius/Sylius/issues/15452) [API] Cover editing and deleting channels ([@TheMilek](https://github.com/TheMilek), [@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15501](https://github.com/Sylius/Sylius/issues/15501) [API] Automatically add a default translation to translatables ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15508](https://github.com/Sylius/Sylius/issues/15508) [ApiBundle] Validate locale field on translation object ([@Wojdylak](https://github.com/Wojdylak)) +- [#15497](https://github.com/Sylius/Sylius/issues/15497) [UserBundle] Implement TTL validation for password reset tokens ([@Wojdylak](https://github.com/Wojdylak)) +- [#15488](https://github.com/Sylius/Sylius/issues/15488) Making the verification and password reset token unqiue ([@mamazu](https://github.com/mamazu)) +- [#15485](https://github.com/Sylius/Sylius/issues/15485) [FIX] Not using `payum/stripe` or `payum/paypal-express-checkout-nvp` ([@Prometee](https://github.com/Prometee)) +- [#15480](https://github.com/Sylius/Sylius/issues/15480) [CS][DX] Refactor ([@github-actions](https://github.com/github-actions)[[@bot](https://github.com/bot)]) +- [#15517](https://github.com/Sylius/Sylius/issues/15517) [API] Disallow removing the translation in default locale ([@GSadee](https://github.com/GSadee)) +- [#15535](https://github.com/Sylius/Sylius/issues/15535) [ApiBundle] Unification of locale in translations ([@Wojdylak](https://github.com/Wojdylak)) +- [#15504](https://github.com/Sylius/Sylius/issues/15504) [API][Admin] Managing taxon images ([@GSadee](https://github.com/GSadee)) +- [#15540](https://github.com/Sylius/Sylius/issues/15540) Add note to UPGRADE file about introducing TaxonImageRepository ([@GSadee](https://github.com/GSadee)) +- [#15546](https://github.com/Sylius/Sylius/issues/15546) [API][Admin] Revert changing taxon image file ([@GSadee](https://github.com/GSadee)) +- [#15542](https://github.com/Sylius/Sylius/issues/15542) Update README.md ([@adrian-olebinski](https://github.com/adrian-olebinski)) +- [#15524](https://github.com/Sylius/Sylius/issues/15524) [ApiBundle] Added remaining APIs for the Promotion resource. ([@Wojdylak](https://github.com/Wojdylak)) +- [#15530](https://github.com/Sylius/Sylius/issues/15530) [API] Cover payment method management ([@TheMilek](https://github.com/TheMilek)) +- [#15532](https://github.com/Sylius/Sylius/issues/15532) [API] Modifying placed order billing & shipping addresses ([@Rafikooo](https://github.com/Rafikooo)) +- [#15552](https://github.com/Sylius/Sylius/issues/15552) Cover "Browsing order" scenarios in API ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15518](https://github.com/Sylius/Sylius/issues/15518) Prevent division by zero ([@cbastienbaron](https://github.com/cbastienbaron)) +- [#15525](https://github.com/Sylius/Sylius/issues/15525) [Maintenance] Add more separation rules to arkitect ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15472](https://github.com/Sylius/Sylius/issues/15472) [API][Addressing] Enhance Country code validation ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15551](https://github.com/Sylius/Sylius/issues/15551) Resolve actions and rules configuration validation ([@Wojdylak](https://github.com/Wojdylak)) +- [#15509](https://github.com/Sylius/Sylius/issues/15509) [API][Admin] Managing product images ([@GSadee](https://github.com/GSadee)) +- [#15576](https://github.com/Sylius/Sylius/issues/15576) Resolve psalm issues ([@Wojdylak](https://github.com/Wojdylak)) +- [#15566](https://github.com/Sylius/Sylius/issues/15566) Cover 'order filtration' scenarios in API ([@TheMilek](https://github.com/TheMilek)) +- [#15579](https://github.com/Sylius/Sylius/issues/15579) [Behat] Unify the Currency Handling in Behat Scenarios ([@Rafikooo](https://github.com/Rafikooo)) +- [#15568](https://github.com/Sylius/Sylius/issues/15568) [ApiBundle] Update validation message for command argument types. ([@Wojdylak](https://github.com/Wojdylak)) +- [#15583](https://github.com/Sylius/Sylius/issues/15583) [Api][Test] Update promotion test response check ([@Wojdylak](https://github.com/Wojdylak)) +- [#15539](https://github.com/Sylius/Sylius/issues/15539) [API][Admin] Promotion coupons management(CRUD) ([@NoResponseMate](https://github.com/NoResponseMate), [@Wojdylak](https://github.com/Wojdylak)) +- [#15516](https://github.com/Sylius/Sylius/issues/15516) [Maintenance][Behat] Add suite isolation ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15595](https://github.com/Sylius/Sylius/issues/15595) Remove redundant denormalization check ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15558](https://github.com/Sylius/Sylius/issues/15558) [ECS] Add Code Checking In the Tests Directory and Implement Fixes For the New Code ([@Rafikooo](https://github.com/Rafikooo)) +- [#15604](https://github.com/Sylius/Sylius/issues/15604) [Composer] Fix Symfony dependencies after upmerge ([@GSadee](https://github.com/GSadee)) +- [#15553](https://github.com/Sylius/Sylius/issues/15553) Cover "Order details" scenarios in API ([@jakubtobiasz](https://github.com/jakubtobiasz), [@Wojdylak](https://github.com/Wojdylak)) +- [#15605](https://github.com/Sylius/Sylius/issues/15605) [Workflow] Add catalog promotion state machine config ([@Wojdylak](https://github.com/Wojdylak)) +- [#15601](https://github.com/Sylius/Sylius/issues/15601) [PHPStan] Baseline File Regeneration ([@Rafikooo](https://github.com/Rafikooo)) +- [#15385](https://github.com/Sylius/Sylius/issues/15385) [Maintenance] Add more deprecations ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15561](https://github.com/Sylius/Sylius/issues/15561) [API]Cover generate promotion coupons ([@Wojdylak](https://github.com/Wojdylak)) +- [#15549](https://github.com/Sylius/Sylius/issues/15549) Validate payment method's gateway configuration ([@TheMilek](https://github.com/TheMilek)) +- [#15581](https://github.com/Sylius/Sylius/issues/15581) [API] Resolve update of translations ([@Wojdylak](https://github.com/Wojdylak)) +- [#15611](https://github.com/Sylius/Sylius/issues/15611) Validate Paypal express checkout sandbox field type ([@TheMilek](https://github.com/TheMilek)) +- [#15610](https://github.com/Sylius/Sylius/issues/15610) `JavaScriptTestHelper`: Interval Increase ([@Rafikooo](https://github.com/Rafikooo)) +- [#15614](https://github.com/Sylius/Sylius/issues/15614) [Admin][UI] Fix csrf error breaking admin form ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15609](https://github.com/Sylius/Sylius/issues/15609) [Maintenance][API] Marking more inapplicable scenarios with no-api ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15620](https://github.com/Sylius/Sylius/issues/15620) Fix CI ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15615](https://github.com/Sylius/Sylius/issues/15615) Use serialized name when displaying an error about a wrong request field type ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15616](https://github.com/Sylius/Sylius/issues/15616) [API] Finish covering taxation scenarios ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15623](https://github.com/Sylius/Sylius/issues/15623) [API][Admin] Cover inventory ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15435](https://github.com/Sylius/Sylius/issues/15435) [Checkout] Prevent extra fields when login in on addressing page ([@Jibbarth](https://github.com/Jibbarth)) +- [#14286](https://github.com/Sylius/Sylius/issues/14286) Prevent eager loading hundreds of countries ([@kayue](https://github.com/kayue)) +- [#15627](https://github.com/Sylius/Sylius/issues/15627) Add an endpoint exposing customer's statistics ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15635](https://github.com/Sylius/Sylius/issues/15635) [API][Behat] covering tests for managing promotions ([@Wojdylak](https://github.com/Wojdylak)) +- [#15636](https://github.com/Sylius/Sylius/issues/15636) [ECS] Ordered Types Fixer ([@Rafikooo](https://github.com/Rafikooo)) +- [#15642](https://github.com/Sylius/Sylius/issues/15642) [Admin] Add official support submenu in admin panel ([@GSadee](https://github.com/GSadee)) +- [#15628](https://github.com/Sylius/Sylius/issues/15628) [PromotionBundle] Update validation process of promotion actions/rules ([@Wojdylak](https://github.com/Wojdylak)) +- [#15646](https://github.com/Sylius/Sylius/issues/15646) [API][Admin] Allow using float for amount in tax rates ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15630](https://github.com/Sylius/Sylius/issues/15630) Finish covering managing product-related resources ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15650](https://github.com/Sylius/Sylius/issues/15650) Update the example response of the Customer's statistics endpoint ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15658](https://github.com/Sylius/Sylius/issues/15658) Improve error handling while sending malformed amount value in the tax rate api resource update operation ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15640](https://github.com/Sylius/Sylius/issues/15640) [PromotionBundle] Deprecate PromotionCouponGeneratorInstructionInterface ([@Wojdylak](https://github.com/Wojdylak)) +- [#15655](https://github.com/Sylius/Sylius/issues/15655) [Unit] `OrderPlacerTrait` Refactor ([@Rafikooo](https://github.com/Rafikooo)) +- [#15637](https://github.com/Sylius/Sylius/issues/15637) [Behat] Implementation Improvements of the `ResponseCheckerInterface` ([@Rafikooo](https://github.com/Rafikooo)) +- [#15662](https://github.com/Sylius/Sylius/issues/15662) Add running "Refactor" workflow daily ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15656](https://github.com/Sylius/Sylius/issues/15656) [AUTO] Updated translations from Crowdin (1.12) ([@SyliusBot](https://github.com/SyliusBot)) +- [#15652](https://github.com/Sylius/Sylius/issues/15652) [UI] Improved Statistics Scenarios ([@Rafikooo](https://github.com/Rafikooo)) +- [#15663](https://github.com/Sylius/Sylius/issues/15663) [ApiBundle][Address] Add address log entries operation ([@Wojdylak](https://github.com/Wojdylak)) +- [#15632](https://github.com/Sylius/Sylius/issues/15632) Cover shipping method management ([@TheMilek](https://github.com/TheMilek)) +- [#15639](https://github.com/Sylius/Sylius/issues/15639) [API][Admin] Finish covering product scenarios ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15660](https://github.com/Sylius/Sylius/issues/15660) [API] Finish covering managing orders ([@TheMilek](https://github.com/TheMilek)) +- [#15657](https://github.com/Sylius/Sylius/issues/15657) [AUTO] Updated translations from Crowdin (1.13) ([@SyliusBot](https://github.com/SyliusBot)) +- [#15617](https://github.com/Sylius/Sylius/issues/15617) Switch JS session Behat driver to the Symfony Panther ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15672](https://github.com/Sylius/Sylius/issues/15672) Typo cart-flow.rst ([@zairigimad](https://github.com/zairigimad)) +- [#15664](https://github.com/Sylius/Sylius/issues/15664) Move validation groups parameters to appropriate bundle configs ([@TheMilek](https://github.com/TheMilek)) +- [#13777](https://github.com/Sylius/Sylius/issues/13777) [Locale] Change language negotiation to RFC 4647 based ([@gseric](https://github.com/gseric)) +- [#15687](https://github.com/Sylius/Sylius/issues/15687) Fix Typo fixtures.rst ([@zairigimad](https://github.com/zairigimad)) +- [#15690](https://github.com/Sylius/Sylius/issues/15690) [Behat] Update test steps to use dynamic date ([@Wojdylak](https://github.com/Wojdylak)) +- [#15695](https://github.com/Sylius/Sylius/issues/15695) [TestApi] Add new way of assert violations ([@Wojdylak](https://github.com/Wojdylak)) +- [#15698](https://github.com/Sylius/Sylius/issues/15698) New year 2024! 🥳 ([@TheMilek](https://github.com/TheMilek)) +- [#15705](https://github.com/Sylius/Sylius/issues/15705) [ApiBundle][ProductVariant] Unification of channelCode in channelPricing ([@Wojdylak](https://github.com/Wojdylak)) +- [#15697](https://github.com/Sylius/Sylius/issues/15697) [Maintenance] Remove obsolete extended type method from form extensions ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15718](https://github.com/Sylius/Sylius/issues/15718) [Documentation] Extend the maintenance date of 1.12 and update future releases ([@GSadee](https://github.com/GSadee)) +- [#15693](https://github.com/Sylius/Sylius/issues/15693) Allow to reset admin user password via CLI ([@Wojdylak](https://github.com/Wojdylak)) +- [#15724](https://github.com/Sylius/Sylius/issues/15724) [API] Add validation of option values for the product variant ([@GSadee](https://github.com/GSadee)) +- [#15728](https://github.com/Sylius/Sylius/issues/15728) [ApiBundle] Move LocaleIsUsedException into ApiBundle ([@Wojdylak](https://github.com/Wojdylak)) +- [#15729](https://github.com/Sylius/Sylius/issues/15729) Extract StateMachine abstraction into its own package ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15712](https://github.com/Sylius/Sylius/issues/15712) Refactor resending order confirmation email and cover it in API ([@TheMilek](https://github.com/TheMilek)) +- [#15707](https://github.com/Sylius/Sylius/issues/15707) [ApiBundle][Admin] Implement Shop User Removal and Password Change ([@Wojdylak](https://github.com/Wojdylak)) +- [#15739](https://github.com/Sylius/Sylius/issues/15739) Fix ZoneRepository BC-Break ([@Rafikooo](https://github.com/Rafikooo)) +- [#15598](https://github.com/Sylius/Sylius/issues/15598) [API] Statistics ([@Rafikooo](https://github.com/Rafikooo), [@TheMilek](https://github.com/TheMilek), [@NoResponseMate](https://github.com/NoResponseMate)) +- [#15730](https://github.com/Sylius/Sylius/issues/15730) Organization of ConsoleCommands/Messages ([@Wojdylak](https://github.com/Wojdylak)) +- [#15715](https://github.com/Sylius/Sylius/issues/15715) Refactor resending shipment confirmation email and cover it in API ([@TheMilek](https://github.com/TheMilek)) +- [#15751](https://github.com/Sylius/Sylius/issues/15751) Adjust some wording in UPGRADE guide ([@stefandoorn](https://github.com/stefandoorn)) +- [#15754](https://github.com/Sylius/Sylius/issues/15754) Update the branch-aliases and inter-dependencies versions ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15740](https://github.com/Sylius/Sylius/issues/15740) Improve setup command ([@TheMilek](https://github.com/TheMilek)) +- [#15761](https://github.com/Sylius/Sylius/issues/15761) [Admin] Add Sylius certification to menu ([@Wojdylak](https://github.com/Wojdylak)) +- [#15755](https://github.com/Sylius/Sylius/issues/15755) [ApiBundle] Refactor commands' constructors ([@Wojdylak](https://github.com/Wojdylak)) +- [#15749](https://github.com/Sylius/Sylius/issues/15749) [AUTO] Updated translations from Crowdin (1.12) +- [#15750](https://github.com/Sylius/Sylius/issues/15750) [AUTO] Updated translations from Crowdin (1.13) +- [#15768](https://github.com/Sylius/Sylius/issues/15768) [Fix] Invalid RemoveShopUserHandlerSpec namespace ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15746](https://github.com/Sylius/Sylius/issues/15746) Add builds checking whether the frontend can be built with all supported nodejs versions ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15771](https://github.com/Sylius/Sylius/issues/15771) Add default locale paramter to core ([@TheMilek](https://github.com/TheMilek)) +- [#15775](https://github.com/Sylius/Sylius/issues/15775) [Maintenance][API] Normalize command missing fields' names ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15777](https://github.com/Sylius/Sylius/issues/15777) Bump the @sylius-ui/frontend version ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15765](https://github.com/Sylius/Sylius/issues/15765) [Maintenance][Statistics] Bit of an overhaul ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15708](https://github.com/Sylius/Sylius/issues/15708) [Promotion] Switch catalog promotion validation from custom solution to symfony ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15770](https://github.com/Sylius/Sylius/issues/15770) [API] Split read serialization group ([@TheMilek](https://github.com/TheMilek)) +- [#15780](https://github.com/Sylius/Sylius/issues/15780) [API] Add BC layer for transition from :read to :index and :show serialization groups ([@TheMilek](https://github.com/TheMilek)) +- [#15738](https://github.com/Sylius/Sylius/issues/15738) Use the new State Machine abstraction ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15796](https://github.com/Sylius/Sylius/issues/15796) Update codeowners ([@jakubtobiasz](https://github.com/jakubtobiasz)) +- [#15798](https://github.com/Sylius/Sylius/issues/15798) Move CartQuantityRuleChecker to CoreBundle ([@Wojdylak](https://github.com/Wojdylak)) +- [#15802](https://github.com/Sylius/Sylius/issues/15802) [Maintenance] Cleanup ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15804](https://github.com/Sylius/Sylius/issues/15804) [StateMachineAbstraction] Add missing LICENSE and README files ([@GSadee](https://github.com/GSadee)) +- [#15805](https://github.com/Sylius/Sylius/issues/15805) Update the license years for the last time ([@GSadee](https://github.com/GSadee)) +- [#15806](https://github.com/Sylius/Sylius/issues/15806) [Documentation] Fix table of supported versions ([@GSadee](https://github.com/GSadee)) +- [#15774](https://github.com/Sylius/Sylius/issues/15774) [Emails] Move email managers from Admin and Shop bundles to CoreBundle ([@GSadee](https://github.com/GSadee)) +- [#15799](https://github.com/Sylius/Sylius/issues/15799) Move ItemTotalRuleChecker to CoreBundle ([@Wojdylak](https://github.com/Wojdylak)) +- [#15735](https://github.com/Sylius/Sylius/issues/15735) Validate resending order confirmation email ([@TheMilek](https://github.com/TheMilek)) +- [#15808](https://github.com/Sylius/Sylius/issues/15808) [Maintenance] Update .gitattributes ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15809](https://github.com/Sylius/Sylius/issues/15809) [Maintenance][Behat] Autoload calendar contexts into container ([@NoResponseMate](https://github.com/NoResponseMate)) +- [#15815](https://github.com/Sylius/Sylius/issues/15815) Update the branch-aliases ([@Wojdylak](https://github.com/Wojdylak)) +- [#15757](https://github.com/Sylius/Sylius/issues/15757) Missing typehint on processDistributionWithMinimumPrice ([@cbastienbaron](https://github.com/cbastienbaron)) +- [#15814](https://github.com/Sylius/Sylius/issues/15814) [AUTO] Updated translations from Crowdin (1.13) diff --git a/CONFLICTS.md b/CONFLICTS.md index e5ecc2bac3..729871d2db 100644 --- a/CONFLICTS.md +++ b/CONFLICTS.md @@ -3,58 +3,17 @@ This document explains why certain conflicts were added to `composer.json` and references related issues. - - `doctrine/doctrine-bundle:2.3.0`: - - This version makes Gedmo Doctrine Extensions fail (tree and position behaviour mostly). - - References: https://github.com/doctrine/DoctrineBundle/issues/1305 - - - `jms/serializer-bundle:4.1.0`: - - This version contains service with a wrong constructor arguments: - `Invalid definition for service ".container.private.profiler": argument 4 of "JMS\SerializerBundle\Debug\DataCollector::__construct()" accepts "JMS\SerializerBundle\Debug\TraceableDriver", "JMS\SerializerBundle\Debug\TraceableMetadataFactory" passed.` - - References: https://github.com/schmittjoh/JMSSerializerBundle/issues/902 - - - `symfony/dependency-injection:5.4.5`: - - This version is causing a problem with mink session: - `InvalidArgumentException: Specify session name to get in vendor/friends-of-behat/mink/src/Mink.php:198`, - - - `symfony/framework-bundle:5.4.5`: - - This version is causing a problem with returning null as token from `Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage` - which leads to wrong solving path prefix by `Sylius\Bundle\ApiBundle\Provider\PathPrefixProvider` in API scenarios - - - `api-platform/core:2.7.0`: - - The FQCN of `ApiPlatform\Core\Metadata\Resource\ResourceNameCollection` has changed to: - `ApiPlatform\Metadata\Resource\ResourceNameCollection` and due to this fact - `Sylius\Bundle\ApiBundle\Swagger\AcceptLanguageHeaderDocumentationNormalizer` - references this class throws an exception - `Class "ApiPlatform\Core\Metadata\Resource\ResourceNameCollection" not found` - -- `doctrine/migrations:3.5.3`: - - This version is causing a problem with migrations and results in throwing a `Doctrine\Migrations\Exception\MetadataStorageError` exception e.g. when executing `sylius:install` command. - References: https://github.com/doctrine/migrations/issues/1302 - - `lexik/jwt-authentication-bundle: ^2.18` After bumping to this version ApiBundle starts failing due to requesting a non-existing `api_platform.openapi.factory.legacy` service. As we are not using this service across the ApiBundle we added this conflict to unlock the builds, until we investigate the problem. -- `symfony/framework-bundle:6.2.8`: - - This version is missing the service alias `validator.expression` - which causes ValidatorException exception to be thrown when using `Expression` constraint. - - `doctrine/orm:>= 2.16.0` This version makes Sylius Fixtures loading fail on the product review fixtures. References: https://github.com/doctrine/orm/issues/10869 -- `symfony/validator:5.4.25 || 6.2.12 || 6.3.1` +- `symfony/validator:5.4.25` This version introduced a bug, causing validation constraints to not work. References: https://github.com/symfony/symfony/issues/50780 diff --git a/README.md b/README.md index bdebcbb06f..cc01ec389e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- +

diff --git a/RoboFile.php b/RoboFile.php index 61a894264e..f8863eb925 100644 --- a/RoboFile.php +++ b/RoboFile.php @@ -92,7 +92,9 @@ class RoboFile extends Tasks $task->exec('rm spec/Security/UserPasswordEncoderSpec.php'); } - $task->exec('vendor/bin/phpspec run --ansi --no-interaction -f dot'); + if (file_exists(sprintf('%s/phpspec.yml', $packagePath)) || file_exists(sprintf('%s/phpspec.yml.dist', $packagePath)) || file_exists(sprintf('%s/phpspec.yaml', $packagePath))) { + $task->exec('vendor/bin/phpspec run --ansi --no-interaction -f dot'); + } if (file_exists(sprintf('%s/phpunit.xml', $packagePath)) || file_exists(sprintf('%s/phpunit.xml.dist', $packagePath))) { $task->exec('vendor/bin/phpunit --colors=always'); diff --git a/UPGRADE-1.13.md b/UPGRADE-1.13.md new file mode 100644 index 0000000000..b6b5946875 --- /dev/null +++ b/UPGRADE-1.13.md @@ -0,0 +1,541 @@ +# UPGRADE FROM `v1.12.X` TO `v1.13.0` + +## Preconditions + +### PHP 8.1 support + +Sylius 1.13 comes with a bump of minimum PHP version to 8.1. We strongly advice to make upgrade process step by step, +so it is highly recommended updating your PHP version being still on Sylius 1.12. After ensuring, that previous step succeed, +you may move forward to the Sylius 1.13 update. + +### Symfony support + +In Sylius 1.13, the minimum supported version of Symfony 6 has been bumped up to 6.4. Sylius 1.13 supports both long-term +supported Symfony versions: 5.4 and 6.4. + +## Main update + +1. Starting with Sylius `1.13` we provided a possibility to use the Symfony Workflow as your State Machine. To allow a smooth transition + we created a new package called `sylius/state-machine-abstraction`, + which provides a configurable abstraction, + allowing you to define which adapter should be used (Winzou State Machine or Symfony Workflow) per graph. + Starting with Sylius `1.13` we configure all existing Sylius' graphs to use the Symfony Workflow. + At the same time, all other graphs are using Winzou State Machine as a default state machine, + and must be configured manually to use the Symfony Workflow. + + Configuration: + ```yaml + sylius_state_machine_abstraction: + graphs_to_adapters_mapping: + : + # e.g. + sylius_order_checkout: symfony_workflow # available adapters: symfony_workflow, winzou_state_machine + + # we can also can the default adapter + default_adapter: symfony_workflow # winzou_state_machine is a default value here + ``` + + > **Note!** Starting with Sylius `2.0` the Symfony Workflow will be the default state machine for all graphs. + +1. A new parameter has been added to specify the validation groups for given gateway factory. + If you have any custom validation groups for your factory, you need to add them to your `config/packages/_sylius.yaml` file. + Also, if you have your own gateway factory and want to add your validation groups you can add another entry to the `validation_groups` configuration node. + It is handled by `GatewayConfigGroupsGenerator` and it resolves the groups based on the passed factory name. + ```yaml + sylius_payum: + gateway_config: + validation_groups: + paypal_express_checkout: + - 'sylius' + - 'sylius_paypal_express_checkout' + stripe_checkout: + - 'sylius' + - 'sylius_stripe_checkout' + your_gateway: + - 'sylius' + - 'your_custom_validation_group' + ``` + +1. There have been a naw parameters added to specify the validation groups for given shipping method rule and calculator. + If you have any custom validation groups for your calculator or rules, you need to add them to your `config/packages/_sylius.yaml` file. + Also, if you have your own shipping method rule or calculator and want to add your validation groups you can add another key to the `validation_groups` parameter. + + ```yaml + sylius_shipping: + shipping_method_rule: + validation_groups: + total_weight_greater_than_or_equal: + - 'sylius' + - 'sylius_shipping_method_rule_total_weight' + order_total_greater_than_or_equal: + - 'sylius' + - 'sylius_shipping_method_rule_order_total' + your_shipping_method_rule: + - 'sylius' + - 'your_custom_validation_group' + shipping_method_calculator: + validation_groups: + flat_rate: + - 'sylius' + - 'sylius_shipping_method_calculator_rate' + per_unit_rate: + - 'sylius' + - 'sylius_shipping_method_calculator_rate' + your_shipping_method_calculator: + - 'sylius' + - 'your_custom_validation_group' + ``` + +1. Class `Sylius\Component\Core\Promotion\Updater\Rule\TotalOfItemsFromTaxonRuleUpdater` has been deprecated, as it is no more used. + +1. Class `Sylius\Component\Core\Promotion\Updater\Rule\ContainsProductRuleUpdater` has been deprecated, as it is no more used. + +1. Class `Sylius\Bundle\AdminBundle\EmailManager\OrderEmailManager` and its interface `Sylius\Bundle\AdminBundle\EmailManager\OrderEmailManagerInterface` + have been deprecated, use `Sylius\Bundle\CoreBundle\Mailer\OrderEmailManager` and `Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface` instead. + +1. Class `Sylius\Bundle\AdminBundle\EmailManager\ShipmentEmailManager` and its interface `Sylius\Bundle\AdminBundle\EmailManager\ShipmentEmailManagerInterface` + have been deprecated, use `Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManager` and `Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface` instead. + +1. Class `Sylius\Bundle\ShopBundle\EmailManager\ContactEmailManager` and its interface `Sylius\Bundle\ShopBundle\EmailManager\ContactEmailManagerInterface` + have been deprecated, use `Sylius\Bundle\CoreBundle\Mailer\ContactEmailManager` and `Sylius\Bundle\CoreBundle\Mailer\ContactEmailManagerInterface` instead. + +1. Class `Sylius\Bundle\ShopBundle\EmailManager\OrderEmailManager` and its interface `Sylius\Bundle\ShopBundle\EmailManager\OrderEmailManagerInterface` + have been deprecated, use `Sylius\Bundle\CoreBundle\Mailer\OrderEmailManager` and `Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface` instead. + +1. Class `Sylius\Bundle\ProductBundle\Form\Type\ProductOptionChoiceType` has been deprecated. + Use `Sylius\Bundle\ProductBundle\Form\Type\ProductOptionAutocompleteType` instead. + +1. Using `parentId` query parameter to generate slug in `Sylius\Bundle\TaxonomyBundle\Controller\TaxonSlugController` has been deprecated. + Use the `parentCode` query parameter instead. + +1. Starting with Sylius 1.13, the `SyliusPriceHistoryPlugin` is included. + If you are currently using the plugin in your project, we recommend following the upgrade guide located [here](UPGRADE-FROM-1.12-WITH-PRICE-HISTORY-PLUGIN-TO-1.13.md). + +1. The `Sylius\Bundle\CoreBundle\CatalogPromotion\Command\RemoveInactiveCatalogPromotion` command and its handler + `Sylius\Bundle\CoreBundle\CatalogPromotion\CommandHandler\RemoveInactiveCatalogPromotionHandler` have been deprecated. + Use `Sylius\Bundle\CoreBundle\CatalogPromotion\Command\RemoveCatalogPromotion` command instead. + +1. Passing `Symfony\Component\Messenger\MessageBusInterface` to `Sylius\Bundle\CoreBundle\CatalogPromotion\Processor\CatalogPromotionRemovalProcessor` + as a second and third argument is deprecated. + +1. Passing `Sylius\Bundle\AdminBundle\EmailManager\OrderEmailManagerInterface` to `Sylius\Bundle\AdminBundle\Action\ResendOrderConfirmationEmailAction` as a second argument is deprecated, use `Sylius\Bundle\CoreBundle\MessageDispatcher\ResendOrderConfirmationEmailDispatcherInterface` instead. + +1. The name of the second argument of `Sylius\Bundle\AdminBundle\Action\ResendOrderConfirmationEmailAction` is deprecated and will be renamed to `$resendOrderConfirmationEmailDispatcher`. + +1. Passing `Sylius\Bundle\AdminBundle\EmailManager\ShipmentEmailManagerInterface` to `Sylius\Bundle\AdminBundle\Action\ResendShipmentConfirmationEmailAction` as a second argument is deprecated, use `Sylius\Bundle\CoreBundle\MessageDispatcher\ResendShipmentConfirmationEmailDispatcherInterface` instead. + +1. The name of the second argument of `Sylius\Bundle\AdminBundle\Action\ResendShipmentConfirmationEmailAction` is deprecated and will be renamed to `$resendShipmentConfirmationEmailDispatcher`. + +1. Passing `Sylius\Bundle\AdminBundle\EmailManager\ShipmentEmailManagerInterface` to `Sylius\Bundle\AdminBundle\EventListener\ShipmentShipListener` as a first argument is deprecated, use `Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface` instead. + +1. Passing `Sylius\Bundle\ShopBundle\EmailManager\OrderEmailManagerInterface` to `Sylius\Bundle\ShopBundle\EventListener\OrderCompleteListener` as a first argument is deprecated, use `Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface` instead. + +1. Passing `Sylius\Bundle\ShopBundle\EmailManager\ContactEmailManagerInterface` to `Sylius\Bundle\ShopBundle\Controller\ContactController` as last argument is deprecated, use `Sylius\Bundle\CoreBundle\Mailer\ContactEmailManagerInterface` instead. + +1. Not passing `Sylius\Bundle\CoreBundle\CatalogPromotion\Announcer\CatalogPromotionRemovalAnnouncerInterface` to `Sylius\Bundle\CoreBundle\CatalogPromotion\Processor\CatalogPromotionRemovalProcessor` + as a second argument is deprecated. + +1. Not passing `Doctrine\Persistence\ObjectManager` to `Sylius\Component\Core\Updater\UnpaidOrdersStateUpdater` + as a fifth argument is deprecated. + +1. Not passing `Symfony\Component\Filesystem\Filesystem` as fourth argument and parameters file path as fifth to `Sylius\Bundle\CoreBundle\Installer\Setup\LocaleSetup` is deprecated and will be prohibited in Sylius 2.0. + +1. To ease customization we've introduced attributes for some services in `1.13`: + - `Sylius\Bundle\OrderBundle\Attribute\AsCartContext` for cart contexts + - `Sylius\Bundle\OrderBundle\Attribute\AsOrderProcessor` for order processors + - `Sylius\Bundle\ProductBundle\Attribute\AsProductVariantResolver` for product variant resolvers + + By default, Sylius still configures them using interfaces, but this way you cannot define a priority. + If you want to define a priority, you need to set the following configuration in your `_sylius.yaml` file: + ```yaml + sylius_core: + autoconfigure_with_attributes: true + ``` + and use one of the new attributes accordingly to the type of your class, e.g.: + ```php + `GET /admin/product-variant-translations/{id}` + - `GET /shop/product-variant-translation/{id}` -> `GET /shop/product-variant-translations/{id}` + +1. Typo in the constraint validator's alias returned by `Sylius\Bundle\ApiBundle\Validator\Constraints\ChangedItemQuantityInCartValidator::validatedBy` has been fixed. + Previously it was `sylius_api_validator_changed_item_guantity_in_cart` and now it is `sylius_api_validator_changed_item_quantity_in_cart`. + +1. The `ApiPlatform\Core\Bridge\Symfony\Bundle\Action\SwaggerUiAction` controller has been removed. + Therefore, the `api_platform.swagger.action.ui` service ID points to the API Platform's `SwaggerUiAction` controller. + +1. The following services have been removed: + * `Sylius\Bundle\ApiBundle\Swagger\AdminAuthenticationTokenDocumentationNormalizer` + * `Sylius\Bundle\ApiBundle\Swagger\ShopAuthenticationTokenDocumentationNormalizer` + * `Sylius\Bundle\ApiBundle\Swagger\ProductDocumentationNormalizer` + * `Sylius\Bundle\ApiBundle\Swagger\ProductImageDocumentationNormalizer` + * `Sylius\Bundle\ApiBundle\Swagger\ProductSlugDocumentationNormalizer` + * `Sylius\Bundle\ApiBundle\Swagger\ProductVariantDocumentationNormalizer` + * `Sylius\Bundle\ApiBundle\Swagger\ShippingMethodDocumentationNormalizer` + * `Sylius\Bundle\ApiBundle\Swagger\PathHiderDocumentationNormalizer` + * `Sylius\Bundle\ApiBundle\Swagger\AcceptLanguageHeaderDocumentationNormalizer` + + Responsibility of these services has been moved to the corresponding services tagged with `sylius.open_api.modifier`: + * `Sylius\Bundle\ApiBundle\OpenApi\Documentation\AdminAuthenticationTokenDocumentationModifier` + * `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ShopAuthenticationTokenDocumentationModifier` + * `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductDocumentationModifier` + * `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductImageDocumentationModifier` + * `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductSlugDocumentationModifier` + * `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ProductVariantDocumentationModifier` + * `Sylius\Bundle\ApiBundle\OpenApi\Documentation\ShippingMethodDocumentationModifier` + * `Sylius\Bundle\ApiBundle\OpenApi\Documentation\PathHiderDocumentationModifier` + * `Sylius\Bundle\ApiBundle\OpenApi\Documentation\AcceptLanguageHeaderDocumentationModifier` + +1. All usages of `ApiPlatform\Core\Api\IriConverterInterface` have been switched to its non-deprecated counterpart `ApiPlatform\Api\IriConverterInterface`. + Due to that, the constructor and usage in the following classes have been changed accordingly: + * `Sylius\Bundle\ApiBundle\Controller\GetProductBySlugAction` + * `Sylius\Bundle\ApiBundle\Controller\UploadAvatarImageAction` + * `Sylius\Bundle\ApiBundle\EventListener\AdminAuthenticationSuccessListener` + * `Sylius\Bundle\ApiBundle\EventListener\AuthenticationSuccessListener` + * `Sylius\Bundle\ApiBundle\Filter\Doctrine\CatalogPromotionChannelFilter` + * `Sylius\Bundle\ApiBundle\Filter\Doctrine\ProductVariantCatalogPromotionFilter` + * `Sylius\Bundle\ApiBundle\Filter\Doctrine\ProductVariantOptionValueFilter` + * `Sylius\Bundle\ApiBundle\Filter\Doctrine\ProductVariantOptionValueFilter` + * `Sylius\Bundle\ApiBundle\Filter\Doctrine\TaxonFilter` + * `Sylius\Bundle\ApiBundle\Serializer\ChannelPriceHistoryConfigDenormalizer` + * `Sylius\Bundle\ApiBundle\Serializer\ProductNormalizer` + * `Sylius\Bundle\ApiBundle\Serializer\ProductVariantNormalizer` + * `Sylius\Bundle\ApiBundle\Serializer\ZoneDenormalizer` + +1. The `Sylius\Bundle\ApiBundle\Filter\Doctrine\CatalogPromotionChannelFilter` service and class has been renamed to `Sylius\Bundle\ApiBundle\Filter\Doctrine\ChannelsAwareChannelFilter`. + +1. The `sylius.api.product_taxon_filter` filter has been removed and its functionality has been superseded by the `sylius.api.search_filter.taxon.code` filter. The usage stays the same. + +1. Update in Translations Handling + + The process for creating or updating translations via the API has been refined. Now, the locale for each translation +is determined directly from its key, making the explicit transmission of the `locale` field redundant. Although the API +continues to support the explicit sending of the `locale` field, it is essential that this explicitly sent locale matches +the key in the translation array. In cases of a mismatch between the key and an explicitly sent locale, the API will +respond with a `Sylius\Bundle\ApiBundle\Exception\TranslationLocaleMismatchException`. + +1. The following Command Handlers constructor signatures have changed: + + `Sylius\Bundle\ApiBundle\CommandHandler\Account\SendAccountRegistrationEmailHandler`: + ```diff + public function __construct( + private UserRepositoryInterface $shopUserRepository, + private ChannelRepositoryInterface $channelRepository, + - private SenderInterface $emailSender, + + private Sylius\Bundle\CoreBundle\Mailer\AccountRegistrationEmailManagerInterface $accountRegistrationEmailManager, + ) { + } + ``` + + `Sylius\Bundle\ApiBundle\CommandHandler\Account\SendAccountVerificationEmailHandler`: + ```diff + public function __construct( + private UserRepositoryInterface $shopUserRepository, + private ChannelRepositoryInterface $channelRepository, + - private SenderInterface $emailSender, + + private Sylius\Bundle\CoreBundle\Mailer\AccountVerificationEmailManagerInterface $accountVerificationEmailManager, + ) { + } + ``` + + `Sylius\Bundle\ApiBundle\CommandHandler\Account\SendResetPasswordEmailHandler`: + ```diff + public function __construct( + - private SenderInterface $emailSender, + private UserRepositoryInterface $shopUserRepository, + private ChannelRepositoryInterface $channelRepository, + + private Sylius\Bundle\CoreBundle\Mailer\ResetPasswordEmailManagerInterface $resetPasswordEmailManager, + ) { + } + ``` + + `Sylius\Bundle\ApiBundle\CommandHandler\Checkout\SendOrderConfirmationHandler`: + ```diff + public function __construct( + - private SenderInterface $emailSender, + private OrderRepositoryInterface $orderRepository, + + private Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface $orderEmailManager, + ) { + } + ``` + + `Sylius\Bundle\ApiBundle\CommandHandler\Checkout\SendShipmentConfirmationEmailHandler`: + ```diff + public function __construct( + - private SenderInterface $emailSender, + private ShipmentRepositoryInterface $shipmentRepository, + + private Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface $shipmentEmailManager, + ) { + } + ``` + + `Sylius\Bundle\ApiBundle\CommandHandler\SendContactRequestHandler`: + ```diff + public function __construct( + - private SenderInterface $emailSender, + private ChannelRepositoryInterface $channelRepository, + + private Sylius\Bundle\CoreBundle\Mailer\ContactEmailManagerInterface $contactEmailManager, + ) { + } + ``` + + `Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account\SendResetPasswordEmailHandler`: + ```diff + public function __construct( + private UserRepositoryInterface $shopUserRepository, + - private SenderInterface $emailSender, + + private Sylius\Bundle\CoreBundle\Mailer\ResetPasswordEmailManagerInterface $resetPasswordEmailManager, + ) { + } + ``` + +1. Disabled product and taxon editing at `/admin/product-taxons/{id}` operation to improve data integrity. To modify a productTaxon, remove the existing association and create a new one. + +1. The keys for adjustment endpoints' responses have been changed from `order_item` to `orderItem` and `order_item_unit` to `orderItemUnit`. + +1. The following shop endpoints for getting the translation resources have been removed: + * `GET `/shop/taxon-translations/{id}` + * `GET `/shop/product-translations/{id}` + * `GET `/shop/product-variant-translations/{id}` + * `GET `/shop/shipping-method-translations/{id}` + + The fields those endpoint were exposing are available on their respective translation subject resources. diff --git a/UPGRADE-FROM-1.12-WITH-PRICE-HISTORY-PLUGIN-TO-1.13-LEGACY.md b/UPGRADE-FROM-1.12-WITH-PRICE-HISTORY-PLUGIN-TO-1.13-LEGACY.md new file mode 100644 index 0000000000..b0b8b44cf9 --- /dev/null +++ b/UPGRADE-FROM-1.12-WITH-PRICE-HISTORY-PLUGIN-TO-1.13-LEGACY.md @@ -0,0 +1,156 @@ +⚙️ Upgrade from Sylius 1.12 with PriceHistoryPlugin to Sylius 1.13 +================================================================== + +We encourage you to use the upgrade instructions based on Rector as it is more convenient and faster to accomplish. +The rector installation guide is available [here](UPGRADE-FROM-1.12-WITH-PRICE-HISTORY-PLUGIN-TO-1.13.md). + +Legacy upgrade +-------------- + +1. Remove the PriceHistoryPlugin from composer.json by running: + + ```bash + + composer remove sylius/price-history-plugin --no-scripts + + ``` + +1. Make sure to remove the following config from your `config/packages/sylius_price_history_plugin.yaml` file: + + ```diff + + - - { resource: "@SyliusPriceHistoryPlugin/config/config.yaml" } + + ``` + + And the route from your `config/routes/sylius_price_history_plugin.yaml`: + + ```diff + + - sylius_price_history_admin: + - resource: '@SyliusPriceHistoryPlugin/config/admin_routing.yaml' + - prefix: '/%sylius_admin.path_name%' + + ``` + + And also remove the plugin from your `packages/bundles.php` file: + + ```diff + + - Sylius\PriceHistoryPlugin\SyliusPriceHistoryPlugin::class => ['all' => true], + + ``` + +1. Update the `Channel` entity to use interface from `Core`: + + ```diff + + - use Sylius\PriceHistoryPlugin\Domain\Model\ChannelInterface; + + use Sylius\Component\Core\Model\ChannelInterface; + + ``` + + Then remove the trait: + + ```diff + + - use ChannelPriceHistoryConfigAwareTrait; + + ``` + +1. Update the `ChannelPricing` entity to use interface from `Core`: + + ```diff + + - use Sylius\PriceHistoryPlugin\Domain\Model\ChannelPricingInterface; + + use Sylius\Component\Core\Model\ChannelPricingInterface; + + ``` + Then remove the trait: + + ```diff + + - use LowestPriceBeforeDiscountAwareTrait; + + ``` + +1. Update your resources configuration for `ChannelPricingLogEntry` and `ChannelPriceHistoryConfig` if you have changed them in your project: + + ```diff + + - sylius_price_history: + - batch_size: 100 + + sylius_core: + + price_history: + + batch_size: 100 + resources: + channel_price_history_config: + classes: + model: App\Entity\ChannelPriceHistoryConfig + ... + channel_pricing_log_entry: + classes: + model: App\Entity\ChannelPricingLogEntry + ... + + ``` + +1. The class `Sylius\PriceHistoryPlugin\Infrastructure\EventSubscriber\ChannelPricingLogEntryEventSubscriber` has been replaced by `Sylius\Bundle\CoreBundle\PriceHistory\EventListener\ChannelPricingLogEntryEventListener`. + +1. Change namespaces from the plugin to correct ones: + + ``` + + Sylius\PriceHistoryPlugin\Application\Checker\ProductVariantLowestPriceDisplayChecker => Sylius\Component\Core\Checker\ProductVariantLowestPriceDisplayChecker + Sylius\PriceHistoryPlugin\Application\Checker\ProductVariantLowestPriceDisplayCheckerInterface => Sylius\Component\Core\Checker\ProductVariantLowestPriceDisplayCheckerInterface + Sylius\PriceHistoryPlugin\Application\Command\ApplyLowestPriceOnChannelPricings => Sylius\Bundle\CoreBundle\PriceHistory\Command\ApplyLowestPriceOnChannelPricings + Sylius\PriceHistoryPlugin\Application\CommandDispatcher\BatchedApplyLowestPriceOnChannelPricingsCommandDispatcher => Sylius\Bundle\CoreBundle\PriceHistory\CommandDispatcher\BatchedApplyLowestPriceOnChannelPricingsCommandDispatcher + Sylius\PriceHistoryPlugin\Application\CommandDispatcher\ApplyLowestPriceOnChannelPricingsCommandDispatcherInterface => Sylius\Bundle\CoreBundle\PriceHistory\CommandDispatcher\ApplyLowestPriceOnChannelPricingsCommandDispatcherInterface + Sylius\PriceHistoryPlugin\Application\CommandHandler\ApplyLowestPriceOnChannelPricingsHandler => Sylius\Bundle\CoreBundle\PriceHistory\CommandHandler\ApplyLowestPriceOnChannelPricingsHandler + Sylius\PriceHistoryPlugin\Application\Logger\PriceChangeLogger => Sylius\Bundle\CoreBundle\PriceHistory\Logger\PriceChangeLogger + Sylius\PriceHistoryPlugin\Application\Logger\PriceChangeLoggerInterface => Sylius\Bundle\CoreBundle\PriceHistory\Logger\PriceChangeLoggerInterface + Sylius\PriceHistoryPlugin\Application\Processor\ProductLowestPriceBeforeDiscountProcessor => Sylius\Bundle\CoreBundle\PriceHistory\Processor\ProductLowestPriceBeforeDiscountProcessor + Sylius\PriceHistoryPlugin\Application\Processor\ProductLowestPriceBeforeDiscountProcessorInterface => Sylius\Bundle\CoreBundle\PriceHistory\Processor\ProductLowestPriceBeforeDiscountProcessorInterface + Sylius\PriceHistoryPlugin\Application\Remover\ChannelPricingLogEntriesRemoverInterface => Sylius\Bundle\CoreBundle\PriceHistory\Remover\ChannelPricingLogEntriesRemoverInterface + Sylius\PriceHistoryPlugin\Application\Templating\Helper\PriceHelper => Sylius\Bundle\CoreBundle\Templating\Helper\PriceHelper + Sylius\PriceHistoryPlugin\Application\Validator\ResourceInputDataPropertiesValidatorInterface => Sylius\Bundle\ApiBundle\Validator\ResourceInputDataPropertiesValidatorInterface + Sylius\PriceHistoryPlugin\Domain\Factory\ChannelFactory => Sylius\Component\Core\Factory\ChannelFactory + Sylius\PriceHistoryPlugin\Domain\Factory\ChannelPricingLogEntryFactory => Sylius\Component\Core\Factory\ChannelPricingLogEntryFactory + Sylius\PriceHistoryPlugin\Domain\Factory\ChannelPricingLogEntryFactoryInterface => Sylius\Component\Core\Factory\ChannelPricingLogEntryFactoryInterface + Sylius\PriceHistoryPlugin\Domain\Model\ChannelInterface => Sylius\Component\Core\Model\ChannelInterface + Sylius\PriceHistoryPlugin\Domain\Model\ChannelPriceHistoryConfig => Sylius\Component\Core\Model\ChannelPriceHistoryConfig + Sylius\PriceHistoryPlugin\Domain\Model\ChannelPricingLogEntry => Sylius\Component\Core\Model\ChannelPricingLogEntry + Sylius\PriceHistoryPlugin\Domain\Model\ChannelPriceHistoryConfigInterface => Sylius\Component\Core\Model\ChannelPriceHistoryConfigInterface + Sylius\PriceHistoryPlugin\Domain\Model\ChannelPricingInterface => Sylius\Component\Core\Model\ChannelPricingInterface + Sylius\PriceHistoryPlugin\Domain\Model\ChannelPricingLogEntryInterface => Sylius\Component\Core\Model\ChannelPricingLogEntryInterface + Sylius\PriceHistoryPlugin\Domain\Model\LowestPriceBeforeDiscountAwareInterface => Sylius\Component\Core\Model\ChannelPricingInterface + Sylius\PriceHistoryPlugin\Domain\Repository\ChannelPricingLogEntryRepositoryInterface => Sylius\Component\Core\Repository\ChannelPricingLogEntryRepositoryInterface + Sylius\PriceHistoryPlugin\Infrastructure\Cli\Command\ClearPriceHistoryCommand => Sylius\Bundle\CoreBundle\PriceHistory\Cli\Command\ClearPriceHistoryCommand + Sylius\PriceHistoryPlugin\Infrastructure\Doctrine\ORM\ChannelPricingLogEntryRepository => Sylius\Component\Core\Repository\ChannelPricingLogEntryRepository + Sylius\PriceHistoryPlugin\Infrastructure\Doctrine\ORM\ChannelPricingLogEntryRepositoryInterface => Sylius\Component\Core\Repository\ChannelPricingLogEntryRepositoryInterface + Sylius\PriceHistoryPlugin\Infrastructure\EntityObserver\CreateLogEntryOnPriceChangeObserver => Sylius\Bundle\CoreBundle\PriceHistory\EntityObserver\CreateLogEntryOnPriceChangeObserver + Sylius\PriceHistoryPlugin\Infrastructure\EntityObserver\EntityObserverInterface => Sylius\Bundle\CoreBundle\PriceHistory\EntityObserver\EntityObserverInterface + Sylius\PriceHistoryPlugin\Infrastructure\EntityObserver\ProcessLowestPricesOnChannelChangeObserver => Sylius\Bundle\CoreBundle\PriceHistory\EntityObserver\ProcessLowestPricesOnChannelChangeObserver + Sylius\PriceHistoryPlugin\Infrastructure\EntityObserver\ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserver => Sylius\Bundle\CoreBundle\PriceHistory\EntityObserver\ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserver + Sylius\PriceHistoryPlugin\Infrastructure\Event\OldChannelPricingLogEntriesEvents => Sylius\Bundle\CoreBundle\PriceHistory\Event\OldChannelPricingLogEntriesEvents + Sylius\PriceHistoryPlugin\Infrastructure\EventListener\OnFlushEntityObserverListener => Sylius\Bundle\CoreBundle\PriceHistory\EventListener\OnFlushEntityObserverListener + Sylius\PriceHistoryPlugin\Infrastructure\EventSubscriber\ChannelPricingLogEntryEventSubscriber => Sylius\Bundle\CoreBundle\PriceHistory\EventListener\ChannelPricingLogEntryEventListener + Sylius\PriceHistoryPlugin\Infrastructure\Form\Extension\ChannelTypeExtension => Sylius\Bundle\CoreBundle\Form\Extension\ChannelTypeExtension + Sylius\PriceHistoryPlugin\Infrastructure\Form\Type\ChannelPriceHistoryConfigType => Sylius\Bundle\CoreBundle\Form\Type\ChannelPriceHistoryConfigType + Sylius\PriceHistoryPlugin\Infrastructure\Provider\ProductVariantsPricesProvider => Sylius\Component\Core\Provider\ProductVariantsPricesProvider + Sylius\PriceHistoryPlugin\Infrastructure\Remover\ChannelPricingLogEntriesRemover' => 'Sylius\Bundle\CoreBundle\PriceHistory\Remover\ChannelPricingLogEntriesRemover' + Sylius\PriceHistoryPlugin\Infrastructure\Serializer\ChannelDenormalizer => Sylius\Bundle\ApiBundle\Serializer\ChannelDenormalizer + Sylius\PriceHistoryPlugin\Infrastructure\Serializer\ChannelPriceHistoryConfigDenormalizer => Sylius\Bundle\ApiBundle\Serializer\ChannelPriceHistoryConfigDenormalizer + Sylius\PriceHistoryPlugin\Infrastructure\Serializer\ProductVariantNormalizer => Sylius\Bundle\ApiBundle\Serializer\ProductVariantNormalizer + Sylius\PriceHistoryPlugin\Infrastructure\Twig\PriceExtension => Sylius\Bundle\CoreBundle\Twig\PriceExtension + Sylius\PriceHistoryPlugin\Infrastructure\Twig\SyliusVersionExtension => Sylius\Bundle\CoreBundle\Twig\SyliusVersionExtension + Sylius\PriceHistoryPlugin\Infrastructure\Validator\ResourceApiInputDataPropertiesValidator => Sylius\Bundle\ApiBundle\Validator\ResourceApiInputDataPropertiesValidator + + ``` + +1. The `Sylius\PriceHistoryPlugin\Application\Calculator\ProductVariantLowestPriceCalculator` class along with its interface has been removed. + If you have used it in your project, you should also remove it from your code. + + Use `Sylius\Component\Core\Calculator\ProductVariantPriceCalculator` instead, as it has been extended with the `calculateLowestPrice` method. + +1. Go through the rest of the [Sylius 1.13 upgrade file](UPGRADE-1.13.md). diff --git a/UPGRADE-FROM-1.12-WITH-PRICE-HISTORY-PLUGIN-TO-1.13.md b/UPGRADE-FROM-1.12-WITH-PRICE-HISTORY-PLUGIN-TO-1.13.md new file mode 100644 index 0000000000..e552b24a0a --- /dev/null +++ b/UPGRADE-FROM-1.12-WITH-PRICE-HISTORY-PLUGIN-TO-1.13.md @@ -0,0 +1,86 @@ +⚙️ Upgrade from Sylius 1.12 with PriceHistoryPlugin to Sylius 1.13 +================================================================== + +We encourage you to use the upgrade instructions based on Rector as it is more convenient and faster to accomplish. +The legacy installation guide is available [here](UPGRADE-FROM-1.12-WITH-PRICE-HISTORY-PLUGIN-TO-1.13-LEGACY.md). + +Upgrade with Rector +------------------- + +1. Remove the PriceHistoryPlugin from composer.json by running: + + ```bash + + composer remove sylius/price-history-plugin --no-scripts + + ``` + +1. Update your `/rector.php` file: + + ```diff + + + use Sylius\SyliusRector\Set\SyliusPriceHistory; + + return static function (RectorConfig $rectorConfig): void { + // ... + - $rectorConfig->sets([SyliusPriceHistory::PRICE_HISTORY_PLUGIN]) + + $rectorConfig->sets([SyliusPriceHistory::UPGRADE_SYLIUS_1_12_WITH_PRICE_HISTORY_PLUGIN_TO_SYLIUS_1_13]); + }; + + ``` + +1. Run: + + ```bash + + vendor/bin/rector + + ``` + +1. Make sure to remove the following config from your `config/packages/sylius_price_history_plugin.yaml` file: + + ```diff + + - - { resource: "@SyliusPriceHistoryPlugin/config/config.yaml" } + + ``` + + And also the route from your `config/routes/sylius_price_history_plugin.yaml`: + + ```diff + + - sylius_price_history_admin: + - resource: '@SyliusPriceHistoryPlugin/config/admin_routing.yaml' + - prefix: '/%sylius_admin.path_name%' + + ``` + +1. Update your resources configuration for `ChannelPricingLogEntry` and `ChannelPriceHistoryConfig` if you have changed them in your project: + + ```diff + + - sylius_price_history: + - batch_size: 100 + + sylius_core: + + price_history: + + batch_size: 100 + resources: + channel_price_history_config: + classes: + model: App\Entity\ChannelPriceHistoryConfig + ... + channel_pricing_log_entry: + classes: + model: App\Entity\ChannelPricingLogEntry + ... + + ``` + +1. The class `Sylius\PriceHistoryPlugin\Infrastructure\EventSubscriber\ChannelPricingLogEntryEventSubscriber` has been replaced by `Sylius\Bundle\CoreBundle\PriceHistory\EventListener\ChannelPricingLogEntryEventListener`. + +1. The `Sylius\PriceHistoryPlugin\Application\Calculator\ProductVariantLowestPriceCalculator` class along with its interface has been removed. + If you have used it in your project, you should also remove it from your code. + + Use `Sylius\Component\Core\Calculator\ProductVariantPriceCalculator` instead, as it has been extended with the `calculateLowestPrice` method. + +1. Go through the rest of the [Sylius 1.13 upgrade file](UPGRADE-1.13.md). diff --git a/app/TestAppKernel.php b/app/TestAppKernel.php index 00289644f7..305b9ba5f2 100644 --- a/app/TestAppKernel.php +++ b/app/TestAppKernel.php @@ -15,6 +15,10 @@ declare(strict_types=1); -@trigger_error('The "TestAppKernel" class located at "app/TestAppKernel.php" is deprecated since Sylius 1.3. Use "Kernel" class located at "src/Kernel.php" instead.', \E_USER_DEPRECATED); +trigger_deprecation( + 'sylius/sylius', + '1.3', + 'The "TestAppKernel" class located at "app/TestAppKernel.php" is deprecated. Use "Kernel" class located at "src/Kernel.php" instead.', +); class_alias(Kernel::class, TestAppKernel::class); diff --git a/app/config/_container_deprecation.php b/app/config/_container_deprecation.php index 8c1da3b6f2..0b9944bac7 100644 --- a/app/config/_container_deprecation.php +++ b/app/config/_container_deprecation.php @@ -15,4 +15,8 @@ declare(strict_types=1); -@trigger_error('Importing files from Sylius/Sylius\'s "app/config" directory is deprecated since Sylius 1.3.', \E_USER_DEPRECATED); +trigger_deprecation( + 'sylius/sylius', + '1.3', + 'Importing files from Sylius/Sylius\'s "app/config" directory is deprecated.', +); diff --git a/app/config/_routing_deprecation.php b/app/config/_routing_deprecation.php index eb251e87d5..562fd29ce1 100644 --- a/app/config/_routing_deprecation.php +++ b/app/config/_routing_deprecation.php @@ -17,6 +17,10 @@ declare(strict_types=1); use Symfony\Component\Routing\RouteCollection; -@trigger_error('Importing files from Sylius/Sylius\'s "app/config" directory is deprecated since Sylius 1.3.', \E_USER_DEPRECATED); +trigger_deprecation( + 'sylius/sylius', + '1.3', + 'Importing files from Sylius/Sylius\'s "app/config" directory is deprecated.', +); return new RouteCollection(); diff --git a/composer-require-checker.json b/composer-require-checker.json index a7730cdaeb..0002a8c244 100644 --- a/composer-require-checker.json +++ b/composer-require-checker.json @@ -23,13 +23,44 @@ "Behat\\Testwork\\Tester\\Setup\\Teardown", "Doctrine\\DBAL\\Platforms\\MySqlPlatform", "Doctrine\\DBAL\\Platforms\\MySQLPlatform", + "Http\\Client\\HttpClient", "HWI\\Bundle\\OAuthBundle\\Connect\\AccountConnectorInterface", "HWI\\Bundle\\OAuthBundle\\OAuth\\Response\\UserResponseInterface", "HWI\\Bundle\\OAuthBundle\\Security\\Core\\User\\OAuthAwareUserProviderInterface", "League\\Flysystem\\FilesystemOperator", - "Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactoryInterface", + "Payum\\Core\\Action\\ActionInterface", + "Payum\\Core\\Action\\GatewayAwareAction", + "Payum\\Core\\Bridge\\Spl\\ArrayObject", + "Payum\\Core\\Exception\\RequestNotSupportedException", + "Payum\\Core\\Extension\\Context", + "Payum\\Core\\Extension\\ExtensionInterface", + "Payum\\Core\\HttpClientInterface", + "Payum\\Core\\Model\\GatewayConfig", + "Payum\\Core\\Model\\GatewayConfigInterface", + "Payum\\Core\\Model\\Identity", + "Payum\\Core\\Model\\Payment", + "Payum\\Core\\Model\\PaymentInterface", + "Payum\\Core\\Payum", + "Payum\\Core\\Request\\Authorize", + "Payum\\Core\\Request\\BaseGetStatus", + "Payum\\Core\\Request\\Capture", + "Payum\\Core\\Request\\Convert", + "Payum\\Core\\Request\\Generic", + "Payum\\Core\\Request\\GetStatusInterface", + "Payum\\Core\\Request\\Notify", + "Payum\\Core\\Security\\CryptedInterface", + "Payum\\Core\\Security\\GenericTokenFactoryInterface", + "Payum\\Core\\Security\\HttpRequestVerifierInterface", + "Payum\\Core\\Security\\TokenInterface", + "Payum\\Core\\Security\\Util\\Random", + "Payum\\Core\\Storage\\AbstractStorage", + "Payum\\Paypal\\ExpressCheckout\\Nvp\\PaypalExpressCheckoutGatewayFactory", + "Payum\\Stripe\\StripeCheckoutGatewayFactory", "PHPUnit\\Framework\\ExpectationFailedException", - "Stripe\\Stripe" + "Psr\\Container\\ContainerInterface", + "Psr\\Http\\Message\\RequestFactoryInterface", + "Psr\\Http\\Message\\StreamFactoryInterface", + "Symfony\\Component\\Security\\Core\\Encoder\\EncoderFactoryInterface" ], "php-core-extensions" : [ "Core", diff --git a/composer.json b/composer.json index 1e3ef50c44..6a71f75f5f 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "sylius/sylius", "type": "library", - "version": "v1.12.14-dev", + "version": "v1.13.0-dev", "description": "E-Commerce platform for PHP, based on Symfony framework.", "homepage": "https://sylius.com", "license": "MIT", @@ -20,7 +20,7 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "ext-dom": "*", "ext-exif": "*", "ext-fileinfo": "*", @@ -29,28 +29,29 @@ "ext-hash": "*", "ext-intl": "*", "ext-json": "*", - "api-platform/core": "^2.6", + "ext-simplexml": "*", + "api-platform/core": "^2.7.10", "babdev/pagerfanta-bundle": "^3.0", "behat/transliterator": "^1.3", "doctrine/collections": "^1.6", "doctrine/common": "^3.2", - "doctrine/dbal": "^2.7 || ^3.0", - "doctrine/doctrine-bundle": "^1.12 || ^2.0", + "doctrine/dbal": "^3.0", + "doctrine/doctrine-bundle": "^1.12 || ^2.3.1", "doctrine/doctrine-migrations-bundle": "^3.0.1", "doctrine/event-manager": "^1.1", "doctrine/inflector": "^1.4 || ^2.0", - "doctrine/migrations": "^3.0", + "doctrine/migrations": "^3.5.5", "doctrine/orm": "^2.13", "doctrine/persistence": "^2.3 || ^3.0", "egulias/email-validator": "^3.1", - "enshrined/svg-sanitize": "^0.15.4 || ^0.16", + "enshrined/svg-sanitize": "^0.16", "fakerphp/faker": "^1.10", "friendsofphp/proxy-manager-lts": "^1.0.7", "friendsofsymfony/rest-bundle": "^3.0", "gedmo/doctrine-extensions": "^3.2", - "guzzlehttp/guzzle": "^6.5", - "guzzlehttp/psr7": "^1.8", - "jms/serializer-bundle": "^4.0", + "guzzlehttp/guzzle": "^6.5 || ^7.0", + "guzzlehttp/psr7": "^2.5", + "jms/serializer-bundle": "^4.2", "knplabs/gaufrette": "^0.10 || ^0.11", "knplabs/knp-gaufrette-bundle": "^0.7 || ^0.8", "knplabs/knp-menu": "^3.1", @@ -58,23 +59,26 @@ "laminas/laminas-stdlib": "^3.3.1", "league/flysystem-bundle": "^2.4", "lexik/jwt-authentication-bundle": "^2.11", - "liip/imagine-bundle": "^2.3", + "liip/imagine-bundle": "^2.10", + "nyholm/psr7": "^1.6", "pagerfanta/pagerfanta": "^3.0", - "payum/payum": "^1.7.2", + "payum/offline": "^1.7.3", "payum/payum-bundle": "^2.5", - "php-http/guzzle6-adapter": "^2.0", + "php-http/httplug": "^2.4", "php-http/message-factory": "^1.0", "psr/cache": "^2.0", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0", "psr/log": "^2.0", "ramsey/uuid": "^4.0", - "sonata-project/block-bundle": "^4.2", + "sonata-project/block-bundle": "^4.2 || ^5.0", "stof/doctrine-extensions-bundle": "^1.4", "sylius-labs/association-hydrator": "^1.1 || ^1.2", "sylius-labs/doctrine-migrations-extra-bundle": "^0.1.4 || ^0.2", "sylius-labs/polyfill-symfony-event-dispatcher": "^1.0.1", "sylius-labs/polyfill-symfony-framework-bundle": "^1.0 || ^1.1", "sylius-labs/polyfill-symfony-security": "^1.1", - "sylius/calendar": "^0.3 || ^0.4", + "sylius/calendar": "^0.3 || ^0.4 || ^0.5", "sylius/fixtures-bundle": "^1.7", "sylius/grid": "^1.11", "sylius/grid-bundle": "^1.11", @@ -84,50 +88,53 @@ "sylius/resource": "^1.9", "sylius/resource-bundle": "^1.9", "sylius/theme-bundle": "^2.1.1 || ^2.3", - "symfony/asset": "^5.4 || ^6.0", - "symfony/config": "^5.4 || ^6.0", - "symfony/console": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", + "symfony/asset": "^5.4.21 || ^6.4", + "symfony/config": "^5.4.21 || ^6.4", + "symfony/console": "^5.4.21 || ^6.4", + "symfony/cache-contracts": "^2.5 || ^3.0", + "symfony/dependency-injection": "^5.4.21 || ^6.4", "symfony/deprecation-contracts": "^2.5", - "symfony/doctrine-bridge": "^5.4 || ^6.0", - "symfony/doctrine-messenger": "^5.4 || ^6.0", - "symfony/event-dispatcher": "^5.4 || ^6.0", - "symfony/expression-language": "^5.4 || ^6.0", - "symfony/filesystem": "^5.4 || ^6.0", - "symfony/finder": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0", - "symfony/framework-bundle": "^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/mailer": "^5.4 || ^6.0", - "symfony/messenger": "^5.4 || ^6.0", + "symfony/doctrine-bridge": "^5.4.21 || ^6.4", + "symfony/doctrine-messenger": "^5.4.21 || ^6.4", + "symfony/event-dispatcher": "^5.4.21 || ^6.4", + "symfony/expression-language": "^5.4.21 || ^6.4", + "symfony/filesystem": "^5.4.21 || ^6.4", + "symfony/finder": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4", + "symfony/framework-bundle": "^5.4.21 || ^6.4", + "symfony/http-client": "^5.4.21 || ^6.4", + "symfony/http-foundation": "^5.4.21 || ^6.4", + "symfony/http-kernel": "^5.4.21 || ^6.4", + "symfony/intl": "^5.4.21 || ^6.4", + "symfony/mailer": "^5.4.21 || ^6.4", + "symfony/messenger": "^5.4.21 || ^6.4", "symfony/monolog-bundle": "^3.5", - "symfony/options-resolver": "^5.4 || ^6.0", - "symfony/password-hasher": "^5.4 || ^6.0", + "symfony/options-resolver": "^5.4.21 || ^6.4", + "symfony/password-hasher": "^5.4.21 || ^6.4", "symfony/polyfill-iconv": "^1.17", "symfony/polyfill-intl-icu": "^1.22", "symfony/polyfill-mbstring": "^1.17", "symfony/polyfill-php80": "^1.17", - "symfony/process": "^5.4 || ^6.0", - "symfony/property-access": "^5.4 || ^6.0", - "symfony/property-info": "^5.4 || ^6.0", - "symfony/proxy-manager-bridge": "^5.4 || ^6.0", - "symfony/routing": "^5.4 || ^6.0", - "symfony/security-bundle": "^5.4 || ^6.0", - "symfony/security-core": "^5.4 || ^6.0", - "symfony/security-csrf": "^5.4 || ^6.0", - "symfony/security-http": "^5.4 || ^6.0", - "symfony/serializer": "^5.4 || ^6.0", + "symfony/process": "^5.4.21 || ^6.4", + "symfony/property-access": "^5.4.21 || ^6.4", + "symfony/property-info": "^5.4.21 || ^6.4", + "symfony/proxy-manager-bridge": "^5.4.21 || ^6.4", + "symfony/routing": "^5.4.21 || ^6.4", + "symfony/security-bundle": "^5.4.21 || ^6.4", + "symfony/security-core": "^5.4.21 || ^6.4", + "symfony/security-csrf": "^5.4.21 || ^6.4", + "symfony/security-http": "^5.4.21 || ^6.4", + "symfony/serializer": "^5.4.21 || ^6.4", "symfony/service-contracts": "^2.5 || ^3.0", - "symfony/string": "^5.4 || ^6.0", - "symfony/templating": "^5.4 || ^6.0", - "symfony/translation": "^5.4 || ^6.0", + "symfony/string": "^5.4.21 || ^6.4", + "symfony/templating": "^5.4.21 || ^6.4", + "symfony/translation": "^5.4.21 || ^6.4", "symfony/translation-contracts": "^2.5", - "symfony/twig-bundle": "^5.4 || ^6.0", - "symfony/validator": "^5.4 || ^6.0", + "symfony/twig-bundle": "^5.4.21 || ^6.4", + "symfony/validator": "^5.4.21 || ^6.4", "symfony/webpack-encore-bundle": "^1.15", - "symfony/yaml": "^5.4 || ^6.0", + "symfony/workflow": "^5.4.21 || ^6.4", + "symfony/yaml": "^5.4.21 || ^6.4", "twig/intl-extra": "^2.12 || ^3.4", "twig/twig": "^2.12 || ^3.3", "webmozart/assert": "^1.9", @@ -179,17 +186,10 @@ "sylius/user-bundle": "self.version" }, "conflict": { - "api-platform/core": "2.7.0", - "doctrine/doctrine-bundle": "2.3.0", - "doctrine/migrations": "3.5.3", "doctrine/orm": ">= 2.16.0", - "jms/serializer-bundle": "4.1.0", "lexik/jwt-authentication-bundle": "^2.18", "stof/doctrine-extensions-bundle": "1.8.0", - "symfony/dependency-injection": "5.4.5", - "symfony/framework-bundle": "5.4.5 || 6.2.8", - "symfony/validator": "5.4.25 || 6.2.12 || 6.3.1", - "liip/imagine-bundle": "2.7.0" + "symfony/validator": "5.4.25" }, "require-dev": { "behat/behat": "^3.6.1", @@ -198,7 +198,7 @@ "consolidation/robo": "^3.0|^4.0", "dmore/behat-chrome-extension": "^1.3", "dmore/chrome-mink-driver": "^2.7", - "doctrine/cache": "^1.10", + "doctrine/cache": "^2.2", "doctrine/data-fixtures": "^1.4", "friends-of-behat/mink": "^1.8", "friends-of-behat/mink-browserkit-driver": "^1.4", @@ -214,26 +214,29 @@ "matthiasnoback/symfony-dependency-injection-test": "^4.2", "mikey179/vfsstream": "^1.6", "mockery/mockery": "^1.4", + "payum/paypal-express-checkout-nvp": "^1.7.3", + "payum/stripe": "^1.7.3", "phparkitect/phparkitect": "^0.2.9", "phpspec/phpspec": "^7.2", + "phpspec/prophecy-phpunit": "^2.0", "phpstan/phpstan": "^1.6", - "phpstan/phpstan-doctrine": "1.3.2", + "phpstan/phpstan-doctrine": "1.3.43", "phpstan/phpstan-symfony": "^1.2", "phpstan/phpstan-webmozart-assert": "^1.1", - "phpunit/phpunit": "^8.5", + "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", "psr/event-dispatcher": "^1.0", - "rector/rector": "^0.12.20", + "rector/rector": "^0.18.0", "robertfausk/behat-panther-extension": "^1.1", - "stripe/stripe-php": "^8.1", + "stripe/stripe-php": "^7.0 || ^8.1", "sylius-labs/coding-standard": "^4.2", "sylius-labs/suite-tags-extension": "~0.1", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/debug-bundle": "^5.4 || ^6.0", - "symfony/dotenv": "^5.4 || ^6.0", + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/debug-bundle": "^5.4.21 || ^6.4", + "symfony/dotenv": "^5.4.21 || ^6.4", "symfony/flex": "^2.4", - "symfony/runtime": "^5.4 || ^6.0", - "symfony/web-profiler-bundle": "^5.4 || ^6.0", + "symfony/runtime": "^5.4.21 || ^6.4", + "symfony/web-profiler-bundle": "^5.4.21 || ^6.4", "symplify/monorepo-builder": "^11.0" }, "suggest": { @@ -257,11 +260,15 @@ "extra": { "symfony": { "allow-contrib": false, - "require": "5.4.*" + "require": "^6.4" + }, + "branch-alias": { + "dev-main": "1.13-dev" } }, "autoload": { "psr-4": { + "Sylius\\Abstraction\\StateMachine\\": "src/Sylius/Abstraction/StateMachine/src", "Sylius\\Behat\\": "src/Sylius/Behat/", "Sylius\\Bundle\\": "src/Sylius/Bundle/", "Sylius\\Component\\": "src/Sylius/Component/" @@ -316,7 +323,8 @@ "spec\\Sylius\\Bundle\\TaxationBundle\\": "src/Sylius/Bundle/TaxationBundle/spec/", "spec\\Sylius\\Bundle\\TaxonomyBundle\\": "src/Sylius/Bundle/TaxonomyBundle/spec/", "spec\\Sylius\\Bundle\\UiBundle\\": "src/Sylius/Bundle/UiBundle/spec/", - "spec\\Sylius\\Bundle\\UserBundle\\": "src/Sylius/Bundle/UserBundle/spec/" + "spec\\Sylius\\Bundle\\UserBundle\\": "src/Sylius/Bundle/UserBundle/spec/", + "Tests\\Sylius\\Abstraction\\StateMachine\\": "src/Sylius/Abstraction/StateMachine/tests" }, "classmap": [ "src/Kernel.php" diff --git a/config/bundles.php b/config/bundles.php index b433583b42..755e692458 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -7,6 +7,7 @@ return [ Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Sylius\Calendar\SyliusCalendarBundle::class => ['all' => true], + Sylius\Abstraction\StateMachine\SyliusStateMachineAbstractionBundle::class => ['all' => true], Sylius\Bundle\OrderBundle\SyliusOrderBundle::class => ['all' => true], Sylius\Bundle\MoneyBundle\SyliusMoneyBundle::class => ['all' => true], Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle::class => ['all' => true], diff --git a/config/packages/_sylius.yaml b/config/packages/_sylius.yaml index fa411e6185..deef9ce200 100644 --- a/config/packages/_sylius.yaml +++ b/config/packages/_sylius.yaml @@ -7,6 +7,8 @@ imports: - { resource: "@SyliusApiBundle/Resources/config/app/config.yaml" } + - { resource: "../parameters.yaml" } + parameters: sylius_core.public_dir: '%kernel.project_dir%/public' diff --git a/config/packages/nyholm_psr7.yaml b/config/packages/nyholm_psr7.yaml new file mode 100644 index 0000000000..f1357233b3 --- /dev/null +++ b/config/packages/nyholm_psr7.yaml @@ -0,0 +1,21 @@ +services: + # Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories) + Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory' + + # Register nyholm/psr7 services for autowiring with HTTPlug factories + Http\Message\MessageFactory: '@nyholm.psr7.httplug_factory' + Http\Message\RequestFactory: '@nyholm.psr7.httplug_factory' + Http\Message\ResponseFactory: '@nyholm.psr7.httplug_factory' + Http\Message\StreamFactory: '@nyholm.psr7.httplug_factory' + Http\Message\UriFactory: '@nyholm.psr7.httplug_factory' + + nyholm.psr7.psr17_factory: + class: Nyholm\Psr7\Factory\Psr17Factory + + nyholm.psr7.httplug_factory: + class: Nyholm\Psr7\Factory\HttplugFactory diff --git a/config/packages/test/_sylius.yaml b/config/packages/test/_sylius.yaml index cd01aaf704..247617feba 100644 --- a/config/packages/test/_sylius.yaml +++ b/config/packages/test/_sylius.yaml @@ -1,2 +1,20 @@ +parameters: + test_default_state_machine_adapter: 'symfony_workflow' + test_sylius_state_machine_adapter: '%env(string:default:test_default_state_machine_adapter:TEST_SYLIUS_STATE_MACHINE_ADAPTER)%' + +sylius_user: + encoder: plaintext + sylius_api: enabled: true + +sylius_state_machine_abstraction: + graphs_to_adapters_mapping: + sylius_catalog_promotion: '%test_sylius_state_machine_adapter%' + sylius_order: '%test_sylius_state_machine_adapter%' + sylius_order_checkout: '%test_sylius_state_machine_adapter%' + sylius_order_payment: '%test_sylius_state_machine_adapter%' + sylius_order_shipping: '%test_sylius_state_machine_adapter%' + sylius_payment: '%test_sylius_state_machine_adapter%' + sylius_product_review: '%test_sylius_state_machine_adapter%' + sylius_shipment: '%test_sylius_state_machine_adapter%' diff --git a/config/packages/test/security.yaml b/config/packages/test/security.yaml index 4071d31995..19ced58cda 100644 --- a/config/packages/test/security.yaml +++ b/config/packages/test/security.yaml @@ -1,6 +1,3 @@ security: password_hashers: - Sylius\Component\User\Model\UserInterface: - algorithm: argon2i - time_cost: 3 - memory_cost: 10 + Sylius\Component\User\Model\UserInterface: plaintext diff --git a/config/packages/workflow.yaml b/config/packages/workflow.yaml new file mode 100644 index 0000000000..2a716ff0a1 --- /dev/null +++ b/config/packages/workflow.yaml @@ -0,0 +1,2 @@ +framework: + workflows: ~ diff --git a/config/parameters.yaml b/config/parameters.yaml new file mode 100644 index 0000000000..d1d8c7b9ac --- /dev/null +++ b/config/parameters.yaml @@ -0,0 +1,2 @@ +parameters: + locale: en_US diff --git a/config/services.yaml b/config/services.yaml index 615506eb5b..8b13789179 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,4 +1 @@ -# Put parameters here that don't need to change on each machine where the app is deployed -# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration -parameters: - locale: en_US + diff --git a/docs/book/contributing/code/patches.rst b/docs/book/contributing/code/patches.rst index dd87eae4ed..75b102284d 100644 --- a/docs/book/contributing/code/patches.rst +++ b/docs/book/contributing/code/patches.rst @@ -14,7 +14,7 @@ Before working on Sylius, set a Symfony friendly environment up with the followi software: * Git -* PHP version 8.0 or above +* PHP version 8.1 or above * MySQL Configure Git diff --git a/docs/book/frontend/overview.rst b/docs/book/frontend/overview.rst index 6321fdc043..b1475e3e0f 100644 --- a/docs/book/frontend/overview.rst +++ b/docs/book/frontend/overview.rst @@ -8,7 +8,7 @@ Overview Requirements ------------ -We recommend using Node.js ``16.x`` as it is the current LTS version. However, Sylius frontend is also compatible with Node.js ``14.x`` and ``18.x``. +We recommend using Node.js ``20.x`` as it is the current LTS version. However, Sylius frontend is also compatible with Node.js ``18.x``. In Sylius, we use ``yarn`` as a package manager, but there are no obstacles to using ``npm``. diff --git a/docs/book/installation/installation.rst b/docs/book/installation/installation.rst index 0e1291af9e..8b56b3bf78 100644 --- a/docs/book/installation/installation.rst +++ b/docs/book/installation/installation.rst @@ -7,7 +7,7 @@ Installation The Sylius main application can serve as an end-user app, as well as a foundation for your custom e-commerce application. -To create your Sylius-based application, first make sure you use PHP 8.0 or higher +To create your Sylius-based application, first make sure you use PHP 8.1 or higher and have `Composer`_ installed. Initiating A New Sylius Project @@ -21,7 +21,7 @@ To begin creating your new project, run this command: .. note:: - Make sure to use PHP ^8.0. Using an older PHP version will result in installing an older version of Sylius. + Make sure to use PHP ^8.1. Using an older PHP version will result in installing an older version of Sylius. This will create a new Symfony project in the ``acme`` directory. Next, move to the project directory: diff --git a/docs/book/installation/requirements.rst b/docs/book/installation/requirements.rst index 90c4bc62df..2212baced6 100644 --- a/docs/book/installation/requirements.rst +++ b/docs/book/installation/requirements.rst @@ -29,7 +29,7 @@ PHP required modules and configuration **PHP version**: +---------------+-----------------------+ -| PHP | ^8.0 | +| PHP | ^8.1 | +---------------+-----------------------+ **PHP extensions**: @@ -62,7 +62,7 @@ Database By default, the database connection is pre-configured to work with a following MySQL configuration: +---------------+-----------------------+ -| MySQL | 5.7+, 8.0+ | +| MySQL | 8.0+ | +---------------+-----------------------+ .. note:: @@ -73,10 +73,10 @@ NPM Package Manager ------------------- Sylius Frontend depends on `npm packages `_ for it to run you need to have Node.js installed. -Current minimum supported version of Node.js is: +Current supported versions of Node.js are: +---------------+-----------------------+ -| Node.js | 14.x | +| Node.js | 18.x, 20.x | +---------------+-----------------------+ Access rights diff --git a/docs/book/orders/checkout.rst b/docs/book/orders/checkout.rst index 9b62e695e7..d65eb65159 100644 --- a/docs/book/orders/checkout.rst +++ b/docs/book/orders/checkout.rst @@ -150,7 +150,7 @@ How to perform the Selecting shipping Step programmatically? Before approaching this step be sure that your Order is in the ``addressed`` state. In this state your order will already have a default ShippingMethod assigned, but in this step you can change it and have everything recalculated automatically. -Firstly either create new (see how in the `Shipments concept `_) or retrieve a **ShippingMethod** +Firstly either create new (see how in the :doc:`Shipments concept `) or retrieve a **ShippingMethod** from the repository to assign it to your order's shipment created defaultly in the addressing step. .. code-block:: php @@ -210,7 +210,7 @@ How to perform the Selecting payment step programmatically? Before this step your Order should be in the ``shipping_selected`` state. It will have a default Payment selected after the addressing step, but in this step you can change it. -Firstly either create new (see how in the `Payments concept `_) or retrieve a **PaymentMethod** +Firstly either create new (see how in the :doc:`Payments concept `) or retrieve a **PaymentMethod** from the repository to assign it to your order's payment created defaultly in the addressing step. .. code-block:: php diff --git a/docs/book/organization/backward-compatibility-promise.rst b/docs/book/organization/backward-compatibility-promise.rst index 3d898e6c18..fa003f1a56 100644 --- a/docs/book/organization/backward-compatibility-promise.rst +++ b/docs/book/organization/backward-compatibility-promise.rst @@ -92,16 +92,19 @@ relevant classes, methods, properties: The deprecation message should indicate the version in which the class/method was deprecated and how the feature was replaced (whenever possible). -A PHP ``\E_USER_DEPRECATED`` error must also be triggered to help people with -the migration: +A PHP deprecation must also be triggered to help people with +the migration, for instance: .. code-block:: php - @trigger_error( - 'XXX() is deprecated since version 2.X and will be removed in 2.Y. Use XXX instead.', - \E_USER_DEPRECATED + trigger_deprecation( + 'sylius/some-package', // package name + '1.x', // package version + 'A is deprecated and will be removed in Sylius 2.0. Use B instead.', // message ); +You should not use the @trigger_error() function. + .. _Semantic Versioning: https://semver.org/ .. _Symfony's Backward Compatibility Promise: https://symfony.com/doc/current/contributing/code/bc.html .. _Symfony's Experimental Features: https://symfony.com/doc/current/contributing/code/experimental.html diff --git a/docs/book/plugins/guide/naming.rst b/docs/book/plugins/guide/naming.rst index 711b0b29c2..3a278dad34 100644 --- a/docs/book/plugins/guide/naming.rst +++ b/docs/book/plugins/guide/naming.rst @@ -38,15 +38,15 @@ That's it! All other files are just a boilerplate to show you what can be done i * ``src/Controller/GreetingController.php`` -* ``src/Resources/config/admin_routing.yml`` +* ``config/admin_routing.yml`` -* ``src/Resources/config/shop_routing.yml`` +* ``config/shop_routing.yml`` -* ``src/Resources/public/greeting.js`` +* ``public/greeting.js`` -* ``src/Resources/views/dynamic_greeting.html.twig`` +* ``templates/dynamic_greeting.html.twig`` -* ``src/Resources/views/static_greeting.html.twig`` +* ``templates/static_greeting.html.twig`` * All files from ``tests/Behat/Page/Shop/`` (with corresponding services) diff --git a/docs/components_and_bundles/bundles/SyliusCustomerBundle/summary.rst b/docs/components_and_bundles/bundles/SyliusCustomerBundle/summary.rst index 7783cd455b..98cd6bdbb7 100644 --- a/docs/components_and_bundles/bundles/SyliusCustomerBundle/summary.rst +++ b/docs/components_and_bundles/bundles/SyliusCustomerBundle/summary.rst @@ -30,6 +30,7 @@ Configuration reference customer_group: classes: model: Sylius\Component\Customer\Model\CustomerGroup + repository: Sylius\Bundle\CustomerBundle\Doctrine\ORM\CustomerGroupRepository interface: Sylius\Component\Customer\Model\CustomerGroupInterface controller: Sylius\Bundle\ResourceBundle\Controller\ResourceController factory: Sylius\Component\Resource\Factory\Factory diff --git a/docs/components_and_bundles/bundles/SyliusOrderBundle/processors.rst b/docs/components_and_bundles/bundles/SyliusOrderBundle/processors.rst index b1cc825f44..416373aaa3 100644 --- a/docs/components_and_bundles/bundles/SyliusOrderBundle/processors.rst +++ b/docs/components_and_bundles/bundles/SyliusOrderBundle/processors.rst @@ -1,13 +1,6 @@ -.. rst-class:: outdated - Processors ========== -.. danger:: - - We're sorry but **this documentation section is outdated**. Please have that in mind when trying to use it. - You can help us making documentation up to date via Sylius Github. Thank you! - Order processors are responsible of manipulating the orders to apply different predefined adjustments or other modifications based on order state. Registering custom processors @@ -35,6 +28,38 @@ Once you have your own :ref:`component_order_processors_order-processor-interfac You can add your own processor to the :ref:`component_order_processors_composite_order_processor` using `sylius.order_processor` +.. note:: + + If services autoconfiguration is enabled, you should register your own processor by adding the ``Sylius\Bundle\OrderBundle\Attribute\AsOrderProcessor`` attribute + on the top of the processor class. + + .. code-block:: php + + channelCode = $channelCode; + } + + public function getChannelCode(): ?string + { + return $this->channelCode; + } + + public function getChannel(): ChannelInterface + { + if ('cli' !== PHP_SAPI || null === $this->channelCode) { + throw new ChannelNotFoundException(); + } + + $channel = $this->channelRepository->findOneByCode($this->channelCode); + + if (null === $channel) { + throw new ChannelNotFoundException(); + } + + return $channel; + } + } + +.. code-block:: php + + // src/Channel/Context/CliBasedChannelContextInterface.php; + addOption('channel', 'c', InputOption::VALUE_OPTIONAL, 'Channel code') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if (null !== $channelCode = $input->getOption('channel')) { + $this->cliBasedChannelContext->setChannelCode($channelCode); + } + + // The subscriber just gets a channel from the channel context + $this->dispatcher->dispatch(new DummyEvent()); + + return Command::SUCCESS; + } + } + +The output of the example is following: + +.. code-block:: bash + + $ bin/console app:dummy -c MAGIC_WEB + Hi! I am Dummy Event Subscriber. I am using Channel Context. + Your channel name is: Magic Web Channel + + $ bin/console app:dummy -c FASHION_WEB + Hi! I am Dummy Event Subscriber. I am using Channel Context. + Your channel name is: Fashion Web Store diff --git a/docs/cookbook/cli/map.rst.inc b/docs/cookbook/cli/map.rst.inc new file mode 100644 index 0000000000..f76833bb3f --- /dev/null +++ b/docs/cookbook/cli/map.rst.inc @@ -0,0 +1 @@ +* :doc:`/cookbook/cli/handle-multiple-channels-in-cli` diff --git a/docs/cookbook/index.rst b/docs/cookbook/index.rst index 94ee35ef04..f10b0558cf 100644 --- a/docs/cookbook/index.rst +++ b/docs/cookbook/index.rst @@ -3,6 +3,16 @@ The Cookbook The Sylius Cookbook is a collection of solution articles helping you with some specific, narrow problems. +CLI +--- + +.. toctree:: + :hidden: + + cli/handle-multiple-channels-in-cli + +.. include:: /cookbook/cli/map.rst.inc + Entities -------- diff --git a/docs/customization/grid.rst b/docs/customization/grid.rst index 88847d5cc0..49e79330d1 100644 --- a/docs/customization/grid.rst +++ b/docs/customization/grid.rst @@ -1,3 +1,4 @@ + Customizing Grids ================= @@ -139,6 +140,24 @@ If you would like to change the link to which an action button is redirecting, t The above grid modification will change the redirect of the ``show`` action to redirect to the shop, instead of admin show. Also the label was changed here. +How to remove label of an action from a grid? +''''''''''''''''''''''''''''''''''''''''''''' + +If you would like to remove label for some actions in any grid, you just need to set its ``labeled`` option to ``false`` like below: + +.. code-block:: yaml + + # config/packages/_sylius.yaml + sylius_grid: + grids: + sylius_admin_product_review: + actions: + item: + delete: + type: delete + options: + labeled: false + How to modify positions of fields, filters and actions in a grid? ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/getting-started-with-sylius/using-api.rst b/docs/getting-started-with-sylius/using-api.rst index 6133a23128..dd40529300 100644 --- a/docs/getting-started-with-sylius/using-api.rst +++ b/docs/getting-started-with-sylius/using-api.rst @@ -127,7 +127,7 @@ and let's take first variant `@id` from list: ], "translations": { "en_US": { - "@id": "/api/v2/shop/product-variant-translation/123889", + "@id": "/api/v2/shop/product-variant-translations/123889", "@type": "ProductVariantTranslation", "id": 123889, "name": "S", diff --git a/ecs.php b/ecs.php index d27a97fb89..b5f9e4db03 100644 --- a/ecs.php +++ b/ecs.php @@ -11,6 +11,7 @@ declare(strict_types=1); +use PhpCsFixer\Fixer\ClassNotation\OrderedTypesFixer; use PhpCsFixer\Fixer\ClassNotation\VisibilityRequiredFixer; use PhpCsFixer\Fixer\Comment\HeaderCommentFixer; use PhpCsFixer\Fixer\LanguageConstruct\ErrorSuppressionFixer; @@ -22,16 +23,16 @@ return static function (ECSConfig $config): void { $config->import('vendor/sylius-labs/coding-standard/ecs.php'); $config->parallel(); - $config->paths(['src/Sylius']); + $config->paths(['src/Sylius', 'tests']); $config->skip([ InlineDocCommentDeclarationSniff::class . '.MissingVariable', InlineDocCommentDeclarationSniff::class . '.NoAssignment', VisibilityRequiredFixer::class => ['*Spec.php'], ErrorSuppressionFixer::class => 'src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/BackwardsCompatibility/CancelOrderStateMachineCallbackPass.php', '**/var/*', - 'src/Sylius/Behat/Service/Converter/IriConverter.php', ]); $config->ruleWithConfiguration(PhpdocSeparationFixer::class, ['groups' => [['Given', 'When', 'Then']]]); + $config->ruleWithConfiguration(OrderedTypesFixer::class, ['null_adjustment' => 'always_last']); $config->ruleWithConfiguration( HeaderCommentFixer::class, [ diff --git a/etc/psalm/LaminasPriorityQueueStub.php b/etc/psalm/LaminasPriorityQueueStub.php deleted file mode 100644 index feb68199c4..0000000000 --- a/etc/psalm/LaminasPriorityQueueStub.php +++ /dev/null @@ -1,348 +0,0 @@ - - */ -class PriorityQueue implements Countable, IteratorAggregate, Serializable -{ - const EXTR_DATA = 0x00000001; - const EXTR_PRIORITY = 0x00000002; - const EXTR_BOTH = 0x00000003; - - /** - * Inner queue class to use for iteration - * @var string - */ - protected $queueClass = SplPriorityQueue::class; - - /** - * Actual items aggregated in the priority queue. Each item is an array - * with keys "data" and "priority". - * @var array - */ - protected $items = []; - - /** - * Inner queue object - * @var SplPriorityQueue - */ - protected $queue; - - /** - * Insert an item into the queue - * - * Priority defaults to 1 (low priority) if none provided. - * - * @param mixed $data - * @param int $priority - * @return PriorityQueue - * - * @psalm-param T $data - * @psalm-return PriorityQueue - */ - public function insert($data, $priority = 1) - { - $priority = (int) $priority; - $this->items[] = [ - 'data' => $data, - 'priority' => $priority, - ]; - $this->getQueue()->insert($data, $priority); - return $this; - } - - /** - * Remove an item from the queue - * - * This is different than {@link extract()}; its purpose is to dequeue an - * item. - * - * This operation is potentially expensive, as it requires - * re-initialization and re-population of the inner queue. - * - * Note: this removes the first item matching the provided item found. If - * the same item has been added multiple times, it will not remove other - * instances. - * - * @param mixed $datum - * @return bool False if the item was not found, true otherwise. - * - * @psalm-param T $datum - */ - public function remove($datum) - { - $found = false; - foreach ($this->items as $key => $item) { - if ($item['data'] === $datum) { - $found = true; - break; - } - } - if ($found) { - unset($this->items[$key]); - $this->queue = null; - - if (! $this->isEmpty()) { - $queue = $this->getQueue(); - foreach ($this->items as $item) { - $queue->insert($item['data'], $item['priority']); - } - } - return true; - } - return false; - } - - /** - * Is the queue empty? - * - * @return bool - */ - public function isEmpty() - { - return (0 === $this->count()); - } - - /** - * How many items are in the queue? - * - * @return int - */ - public function count() - { - return count($this->items); - } - - /** - * Peek at the top node in the queue, based on priority. - * - * @return mixed - */ - public function top() - { - return $this->getIterator()->top(); - } - - /** - * Extract a node from the inner queue and sift up - * - * @return mixed - */ - public function extract() - { - $value = $this->getQueue()->extract(); - - $keyToRemove = null; - $highestPriority = null; - foreach ($this->items as $key => $item) { - if ($item['data'] !== $value) { - continue; - } - - if (null === $highestPriority) { - $highestPriority = $item['priority']; - $keyToRemove = $key; - continue; - } - - if ($highestPriority >= $item['priority']) { - continue; - } - - $highestPriority = $item['priority']; - $keyToRemove = $key; - } - - if ($keyToRemove !== null) { - unset($this->items[$keyToRemove]); - } - - return $value; - } - - /** - * Retrieve the inner iterator - * - * SplPriorityQueue acts as a heap, which typically implies that as items - * are iterated, they are also removed. This does not work for situations - * where the queue may be iterated multiple times. As such, this class - * aggregates the values, and also injects an SplPriorityQueue. This method - * retrieves the inner queue object, and clones it for purposes of - * iteration. - * - * @return SplPriorityQueue - * - * @psalm-return SplPriorityQueue - */ - public function getIterator() - { - $queue = $this->getQueue(); - return clone $queue; - } - - /** - * Serialize the data structure - * - * @return string - */ - public function serialize() - { - return serialize($this->items); - } - - /** - * Unserialize a string into a PriorityQueue object - * - * Serialization format is compatible with {@link Laminas\Stdlib\SplPriorityQueue} - * - * @param string $data - * @return void - */ - public function unserialize($data) - { - foreach (unserialize($data) as $item) { - $this->insert($item['data'], $item['priority']); - } - } - - /** - * Serialize to an array - * - * By default, returns only the item data, and in the order registered (not - * sorted). You may provide one of the EXTR_* flags as an argument, allowing - * the ability to return priorities or both data and priority. - * - * @param int $flag - * @return array - */ - public function toArray($flag = self::EXTR_DATA) - { - switch ($flag) { - case self::EXTR_BOTH: - return $this->items; - case self::EXTR_PRIORITY: - return array_map(function ($item) { - return $item['priority']; - }, $this->items); - case self::EXTR_DATA: - default: - return array_map(function ($item) { - return $item['data']; - }, $this->items); - } - } - - /** - * Specify the internal queue class - * - * Please see {@link getIterator()} for details on the necessity of an - * internal queue class. The class provided should extend SplPriorityQueue. - * - * @param string $class - * @return PriorityQueue - * - * @psalm-return PriorityQueue - */ - public function setInternalQueueClass($class) - { - $this->queueClass = (string) $class; - return $this; - } - - /** - * Does the queue contain the given datum? - * - * @param mixed $datum - * @return bool - * - * @psalm-param T $datum - */ - public function contains($datum) - { - foreach ($this->items as $item) { - if ($item['data'] === $datum) { - return true; - } - } - return false; - } - - /** - * Does the queue have an item with the given priority? - * - * @param int $priority - * @return bool - */ - public function hasPriority($priority) - { - foreach ($this->items as $item) { - if ($item['priority'] === $priority) { - return true; - } - } - return false; - } - - /** - * Get the inner priority queue instance - * - * @throws Exception\DomainException - * @return SplPriorityQueue - */ - protected function getQueue() - { - if (null === $this->queue) { - $this->queue = new $this->queueClass(); - if (! $this->queue instanceof \SplPriorityQueue) { - throw new Exception\DomainException(sprintf( - 'PriorityQueue expects an internal queue of type SplPriorityQueue; received "%s"', - get_class($this->queue) - )); - } - } - return $this->queue; - } - - /** - * Add support for deep cloning - * - * @return void - */ - public function __clone() - { - if (null !== $this->queue) { - $this->queue = clone $this->queue; - } - } -} diff --git a/features/account/customer_account/address_book/adding_address_validation.feature b/features/account/customer_account/address_book/adding_address_validation.feature index a56558efff..ac73c5bd4f 100644 --- a/features/account/customer_account/address_book/adding_address_validation.feature +++ b/features/account/customer_account/address_book/adding_address_validation.feature @@ -18,7 +18,7 @@ Feature: Seeing validation messages during address addition Then I should still be on the address addition page And I should be notified about 6 errors - @ui @javascript @api + @ui @api Scenario: The province needs to be selected when the chosen country has at least one stated When I want to add a new address to my address book And I specify the address as "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States", "Arkansas" diff --git a/features/account/customer_account/address_book/address_province_validation.feature b/features/account/customer_account/address_book/address_province_validation.feature index dc175bb642..cf697cd92a 100644 --- a/features/account/customer_account/address_book/address_province_validation.feature +++ b/features/account/customer_account/address_book/address_province_validation.feature @@ -13,7 +13,7 @@ Feature: Province field entry stays after validation errors And I have an address "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States", "Arkansas" in my address book And I have an address "Fletcher Ren", "Upper Barkly Street", "3377", "Ararat", "Australia", "Victoria" in my address book - @ui @javascript @api + @ui @api Scenario: The province name stays after validation error Given I am editing the address of "Lucifer Morningstar" When I remove the street @@ -21,7 +21,7 @@ Feature: Province field entry stays after validation errors Then I should still be on the "Lucifer Morningstar" address edit page And I should still have "Arkansas" as my specified province - @ui @javascript @api + @ui @api Scenario: The selected province stays after validation error Given I am editing the address of "Fletcher Ren" When I remove the street diff --git a/features/account/customer_account/editing_customer_profile.feature b/features/account/customer_account/editing_customer_profile.feature index 72d5e98925..54c60cf510 100644 --- a/features/account/customer_account/editing_customer_profile.feature +++ b/features/account/customer_account/editing_customer_profile.feature @@ -6,7 +6,7 @@ Feature: Editing a customer profile Background: Given the store operates on a single channel in "United States" - And there is a customer "Francis Underwood" identified by an email "francis@underwood.com" and a password "sylius" + And there is a user "francis@underwood.com" identified by "sylius" And I am logged in as "francis@underwood.com" @ui @api @@ -18,17 +18,24 @@ Feature: Editing a customer profile Then I should be notified that it has been successfully edited And my name should be "Will Conway" - @ui @email + @ui @email @no-api Scenario: Changing my email if channel requires verification When I want to modify my profile And I specify the customer email as "frank@underwood.com" And I save my changes -# Then I should be notified that it has been successfully edited -# And I should be notified that the verification email has been sent + Then I should be notified that it has been successfully edited + And I should be notified that the verification email has been sent And it should be sent to "frank@underwood.com" And I should not be logged in - @ui + @api @no-ui + Scenario: Changing my email if channel requires verification + When I want to modify my profile + And I specify the customer email as "frank@underwood.com" + And I save my changes + And I should not be logged in + + @ui @no-api Scenario: Changing my email if channel does not require verification Given "United States" channel has account verification disabled When I want to modify my profile @@ -38,7 +45,7 @@ Feature: Editing a customer profile And my account should not be verified And my email should be "frank@underwood.com" - @api + @api @no-ui Scenario: Changing my email if channel does not require verification Given "United States" channel has account verification disabled When I want to modify my profile diff --git a/features/account/customer_account/viewing_orders_history/viewing_order_only_from_current_channel.feature b/features/account/customer_account/viewing_orders_history/viewing_order_only_from_current_channel.feature index a956078551..a9eb6173c0 100644 --- a/features/account/customer_account/viewing_orders_history/viewing_order_only_from_current_channel.feature +++ b/features/account/customer_account/viewing_orders_history/viewing_order_only_from_current_channel.feature @@ -12,8 +12,8 @@ Feature: Viewing orders only from current channel And the store has a zone "United States + United Kingdom" with code "US + UK" And this zone has the "United States" country member And this zone has the "United Kingdom" country member - And the store has a product "Angel T-Shirt" priced at "$100" in "Web-US" channel - And this product is also priced at "£200" in "Web-UK" channel + And the store has a product "Angel T-Shirt" priced at "$100.00" in "Web-US" channel + And this product is also priced at "£200.00" in "Web-UK" channel And the store ships everywhere for free for all channels And the store allows paying Offline for all channels And there is a customer "John Hancock" identified by an email "hancock@superheronope.com" and a password "superPower" diff --git a/features/account/receiving_email_after_registration.feature b/features/account/receiving_email_after_registration.feature index 29b54cf384..cd8cbe2f14 100644 --- a/features/account/receiving_email_after_registration.feature +++ b/features/account/receiving_email_after_registration.feature @@ -9,11 +9,29 @@ Feature: Receiving a welcoming email after registration And that channel allows to shop using "English (United States)" and "Polish (Poland)" locales @ui @email @api - Scenario: Receiving a welcoming email after registration + Scenario: Receiving a welcoming email after registration when channel has disabled registration verification + Given on this channel account verification is not required When I register with email "ghastly@bespoke.com" and password "suitsarelife" - Then a welcoming email should have been sent to "ghastly@bespoke.com" + Then only one email should have been sent to "ghastly@bespoke.com" + And a welcoming email should have been sent to "ghastly@bespoke.com" + + @ui @email @api + Scenario: Receiving an account verification email after registration when channel has enabled registration verification + Given on this channel account verification is required + When I register with email "ghastly@bespoke.com" and password "suitsarelife" + Then only one email should have been sent to "ghastly@bespoke.com" + And a verification email should have been sent to "ghastly@bespoke.com" + But a welcoming email should not have been sent to "ghastly@bespoke.com" @ui @email @api Scenario: Receiving a welcoming email after registration in different locale than the default one + Given on this channel account verification is not required When I register with email "ghastly@bespoke.com" and password "suitsarelife" in the "Polish (Poland)" locale Then a welcoming email should have been sent to "ghastly@bespoke.com" in "Polish (Poland)" locale + + @ui @email @api + Scenario: Receiving a welcoming email after account verification when channel has enabled registration verification + Given on this channel account verification is required + And I register with email "ghastly@bespoke.com" and password "suitsarelife" + When I verify my account using link sent to "ghastly@bespoke.com" + Then a welcoming email should have been sent to "ghastly@bespoke.com" diff --git a/features/account/registering.feature b/features/account/registering.feature index 4f497d69a9..ec78e14204 100644 --- a/features/account/registering.feature +++ b/features/account/registering.feature @@ -7,8 +7,23 @@ Feature: Account registration Background: Given the store operates on a single channel in "United States" - @ui @api - Scenario: Registering a new account with minimum information + @ui @no-api + Scenario: Registering a new account with minimum information when channel has enabled registration verification + Given on this channel account verification is required + When I want to register a new account + And I specify the first name as "Saul" + And I specify the last name as "Goodman" + And I specify the email as "goodman@gmail.com" + And I specify the password as "heisenberg" + And I confirm this password + And I register this account + Then I should be notified that new account has been successfully created + And I should be on registration thank you page + But I should not be logged in + + @api @no-ui + Scenario: Registering a new account with minimum information when channel has enabled registration verification + Given on this channel account verification is required When I want to register a new account And I specify the first name as "Saul" And I specify the last name as "Goodman" @@ -31,6 +46,7 @@ Feature: Account registration And I register this account Then I should be notified that new account has been successfully created And I should be logged in + And I should be on my account dashboard @ui @api Scenario: Registering a new account with all details diff --git a/features/account/resetting_password.feature b/features/account/resetting_password.feature index a1e4d7e022..59fbb34c1d 100644 --- a/features/account/resetting_password.feature +++ b/features/account/resetting_password.feature @@ -48,3 +48,13 @@ Feature: Resetting a password And I confirm my new password as "newp@ssw0rd" And I reset it Then I should not be able to change my password again with the same token + + @ui @email @api + Scenario: Trying to change my account password with an expired token I received + Given I have already received a resetting password email + But I waited too long, and the token expired + When I follow link on my email to reset my password + And I specify my new password as "newp@ssw0rd" + And I confirm my new password as "newp@ssw0rd" + And I reset it + Then I should not be able to change my password with this token diff --git a/features/account/signing_in_validation.feature b/features/account/signing_in_validation.feature index fc302a5cfe..43283d3c21 100644 --- a/features/account/signing_in_validation.feature +++ b/features/account/signing_in_validation.feature @@ -17,14 +17,14 @@ Feature: Signing in to the store validation Then I should be notified about bad credentials And I should not be logged in - @ui + @ui @api Scenario: Trying to sign in without confirming account When I register with email "sylius@example.com" and password "sylius" And I want to log in And I specify the username as "sylius@example.com" And I specify the password as "sylius" And I try to log in - Then I should be notified about disabled account + Then I should be notified about bad credentials And I should not be logged in @ui @api diff --git a/features/account/verification/verifying_email_address.feature b/features/account/verification/verifying_email_address.feature index cabed7a609..b3c2f8e2c2 100644 --- a/features/account/verification/verifying_email_address.feature +++ b/features/account/verification/verifying_email_address.feature @@ -49,7 +49,7 @@ Feature: Verifying account's email address Scenario: Receiving account verification email after registration When I register with email "ghastly@bespoke.com" and password "suitsarelife" Then I should be notified that my account has been created and the verification email has been sent - And 2 emails should be sent to "ghastly@bespoke.com" + And 1 email should be sent to "ghastly@bespoke.com" But I should not be able to log in as "ghastly@bespoke.com" with "suitsarelife" password @ui @email @api diff --git a/features/addressing/managing_countries/country_unique_code_validation.feature b/features/addressing/managing_countries/country_unique_code_validation.feature deleted file mode 100644 index 974f6165ee..0000000000 --- a/features/addressing/managing_countries/country_unique_code_validation.feature +++ /dev/null @@ -1,14 +0,0 @@ -@managing_countries -Feature: Country unique code validation - In order to avoid making mistakes when managing countries - As an Administrator - I want to be prevented from adding a new country with an existing code - - Background: - Given the store operates in "Norway" - And I am logged in as an administrator - - @ui @api - Scenario: Trying to add a new country with used code - When I want to add a new country - Then I should not be able to choose "Norway" diff --git a/features/addressing/managing_countries/country_validation.feature b/features/addressing/managing_countries/country_validation.feature new file mode 100644 index 0000000000..9815e50c69 --- /dev/null +++ b/features/addressing/managing_countries/country_validation.feature @@ -0,0 +1,35 @@ +@managing_countries +Feature: Country validation + In order to avoid making mistakes when managing countries + As an Administrator + I want to be prevented from adding a new country with invalid code + + Background: + Given the store operates in "Norway" + And I am logged in as an administrator + + @ui @api + Scenario: Trying to add a new country with used code + When I want to add a new country + Then I should not be able to choose "Norway" + + @api @no-ui + Scenario: Trying to add a new country with invalid code + When I want to add a new country + And I specify the country code as "NJ" + And I try to save my changes + Then I should be notified that the country code is invalid + + @api @no-ui + Scenario: Trying to add a new country with alpha-3 code + When I want to add a new country + And I specify the country code as "USA" + And I try to save my changes + Then I should be notified that the country code is invalid + + @api @no-ui + Scenario: Trying to add a new country with no code + When I want to add a new country + And I do not specify the country code + And I try to save my changes + Then I should be notified that the country code is required diff --git a/features/addressing/managing_countries/province_unique_fields_validation.feature b/features/addressing/managing_countries/province_unique_fields_validation.feature index 94bd519397..6c10b40638 100644 --- a/features/addressing/managing_countries/province_unique_fields_validation.feature +++ b/features/addressing/managing_countries/province_unique_fields_validation.feature @@ -21,7 +21,7 @@ Feature: Province unique fields validation Scenario: Trying to add a new province with a taken name When I want to edit this country And I add the "Northern Ireland" province with "GB-NI" code - And I try to save changes + And I save my changes Then I should be notified that province name must be unique @ui @javascript @api @@ -29,7 +29,7 @@ Feature: Province unique fields validation When I want to edit this country And I add the "Scotland" province with "GB-SCO" code And I add the "Not Scotland" province with "GB-SCO" code - And I try to save changes + And I save my changes Then I should be notified that all province codes and names within this country need to be unique @ui @javascript @api @@ -37,5 +37,5 @@ Feature: Province unique fields validation When I want to edit this country And I add the "Scotland" province with "GB-SC" code And I add the "Scotland" province with "GB-SCO" code - And I try to save changes + And I save my changes Then I should be notified that all province codes and names within this country need to be unique diff --git a/features/addressing/managing_countries/province_validation.feature b/features/addressing/managing_countries/province_validation.feature index dc08795ce9..e813d989a2 100644 --- a/features/addressing/managing_countries/province_validation.feature +++ b/features/addressing/managing_countries/province_validation.feature @@ -13,7 +13,7 @@ Feature: Province validation When I want to create a new province in country "United Kingdom" And I name the province "Scotland" But I do not specify the province code - And I try to save changes + And I try to save my changes Then I should be notified that code is required And province with name "Scotland" should not be added in this country @@ -22,7 +22,7 @@ Feature: Province validation When I want to create a new province in country "United Kingdom" And I specify the province code as "GB-SCT" But I do not name the province - And I try to save changes + And I try to save my changes Then I should be notified that name is required And province with code "GB-SCT" should not be added in this country diff --git a/features/addressing/managing_zones/deleting_multiple_zones.feature b/features/addressing/managing_zones/deleting_multiple_zones.feature index 28995ff551..5f8936dfac 100644 --- a/features/addressing/managing_zones/deleting_multiple_zones.feature +++ b/features/addressing/managing_zones/deleting_multiple_zones.feature @@ -8,7 +8,7 @@ Feature: Deleting multiple zones Given the store has zones "North America", "South America" and "Europe" And I am logged in as an administrator - @ui @api @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple zones at once When I browse zones And I check the "North America" zone diff --git a/features/admin/being_redirected_to_previous_filtered_page.feature b/features/admin/being_redirected_to_previous_filtered_page.feature index 09c791e77d..c75a1e5e0f 100644 --- a/features/admin/being_redirected_to_previous_filtered_page.feature +++ b/features/admin/being_redirected_to_previous_filtered_page.feature @@ -39,7 +39,7 @@ Feature: Being redirected to previous filtered page And I cancel my changes Then I should be redirected to the 2nd page of only enabled products - @ui @javascript @no-api + @ui @no-api Scenario: Being redirected to previous filtered page after cancelling creating a new product When I browse products And I choose enabled filter diff --git a/features/admin/dashboard.feature b/features/admin/dashboard.feature deleted file mode 100644 index 9ccdd7bf47..0000000000 --- a/features/admin/dashboard.feature +++ /dev/null @@ -1,44 +0,0 @@ -@admin_dashboard -Feature: Statistics dashboard in a single channel - In order to have an overview of my sales - As an Administrator - I want to see overall statistics on my admin dashboard - - Background: - Given the store operates on a single channel in "United States" - And the store ships everywhere for Free - And the store allows paying Offline - And the store has a product "Sylius T-Shirt" - And this product has "Red XL" variant priced at "$40" - And I am logged in as an administrator - - @ui - Scenario: Seeing basic statistics for entire store - Given 3 customers have fulfilled 4 orders placed for total of "$8566.00" - And then 2 more customers have paid 2 orders placed for total of "$459.00" - When I open administration dashboard - Then I should see 6 new orders - And I should see 5 new customers - And there should be total sales of "$9,025.00" - And the average order value should be "$1,504.17" - - @ui - Scenario: Statistics include only fulfilled orders that were not cancelled - Given 4 customers have fulfilled 4 orders placed for total of "$5241.00" - And then 2 more customers have placed 2 orders for total of "$459.00" - And 2 customers have added products to the cart for total of "$3450.00" - And a single customer has placed an order for total of "$1000.00" - But the customer cancelled this order - When I open administration dashboard - Then I should see 4 new orders - And I should see 9 new customers - And there should be total sales of "$5,241.00" - And the average order value should be "$1,310.25" - - @ui - Scenario: Seeing recent orders and customers - Given 2 customers have placed 3 orders for total of "$340.00" - And 2 customers have added products to the cart for total of "$424.00" - When I open administration dashboard - Then I should see 4 new customers in the list - And I should see 3 new orders in the list diff --git a/features/admin/dashboard_per_channel.feature b/features/admin/dashboard_per_channel.feature deleted file mode 100644 index 4db7697097..0000000000 --- a/features/admin/dashboard_per_channel.feature +++ /dev/null @@ -1,50 +0,0 @@ -@admin_dashboard -Feature: Statistics dashboard per channel - In order to have an overview of my sales - As an Administrator - I want to see overall statistics on my admin dashboard in a specific channel - - Background: - Given the store operates on a channel named "Poland" - And there is product "Onion" available in this channel - And the store operates on another channel named "United States" - And there is product "Banana" available in that channel - And the store ships everywhere for Free - And the store allows paying Offline - And I am logged in as an administrator - - @ui - Scenario: Seeing basic statistics for the first channel by default - Given 3 customers have fulfilled 4 orders placed for total of "$8566.00" mostly "Onion" product - And then 2 more customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product - When I open administration dashboard - Then I should see 4 new orders - And I should see 5 new customers - And there should be total sales of "$8,566.00" - And the average order value should be "$2,141.50" - - @ui - Scenario: Changing channel in administration dashboard - Given 4 customers have fulfilled 4 orders placed for total of "$5241.00" mostly "Onion" product - And then 2 more customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product - And then 2 more customers have placed 3 orders for total of "$1259.00" mostly "Banana" product - When I open administration dashboard - And I choose "United States" channel - Then I should see 2 new orders - And I should see 8 new customers - And there should be total sales of "$459.00" - And the average order value should be "$229.50" - - @ui - Scenario: Seeing recent orders in a specific channel - Given 3 customers have placed 4 orders for total of "$8566.00" mostly "Onion" product - And then 2 more customers have placed 2 orders for total of "$459.00" mostly "Banana" product - When I open administration dashboard for "Poland" channel - Then I should see 4 new orders in the list - - @ui - Scenario: Seeing recent orders in a specific channel - Given 3 customers have placed 4 orders for total of "$8566.00" mostly "Onion" product - And then 2 more customers have placed 2 orders for total of "$459.00" mostly "Banana" product - When I open administration dashboard for "United States" channel - Then I should see 2 new orders in the list diff --git a/features/admin/redirecting_on_login_after_being_logout.feature b/features/admin/redirecting_on_login_after_being_logout.feature index 17ae72218b..f370b73f79 100644 --- a/features/admin/redirecting_on_login_after_being_logout.feature +++ b/features/admin/redirecting_on_login_after_being_logout.feature @@ -8,7 +8,7 @@ Feature: Redirecting on login page Given the store operates on a single channel in "United States" And I have been logged out from administration - @ui + @ui @no-api Scenario: Redirecting on login page after being logout When I try to open administration dashboard Then I should be on login page diff --git a/features/admin/securing_access_to_account_after_using_back_button_after_logging_out.feature b/features/admin/securing_access_to_account_after_using_back_button_after_logging_out.feature index cf72ee6064..a97ac4017f 100644 --- a/features/admin/securing_access_to_account_after_using_back_button_after_logging_out.feature +++ b/features/admin/securing_access_to_account_after_using_back_button_after_logging_out.feature @@ -7,11 +7,11 @@ Feature: Securing access to the administration panel after using the back button Background: Given the store operates on a single channel in "United States" And I am logged in as an administrator - And I am on the administration dashboard @ui @mink:chromedriver @no-api Scenario: Securing access to administration dashboard after using the back button after logging out - When I log out + When I am on the administration dashboard + And I log out And I go back one page in the browser Then I should not see the administration dashboard And I should be on the login page diff --git a/features/admin/statistics.feature b/features/admin/statistics.feature new file mode 100644 index 0000000000..71100eb2ac --- /dev/null +++ b/features/admin/statistics.feature @@ -0,0 +1,67 @@ +@admin_dashboard +Feature: Statistics + In order to gain insight into my sales performance and customers activity + As an Administrator + I want to view comprehensive statistics + + Background: + Given the store operates on a single channel in "United States" + And the store ships everywhere for Free + And the store allows paying Offline + And the store has a product "Sylius T-Shirt" + And this product has "Red XL" variant priced at "$40.00" + And I am logged in as an administrator + + @ui @no-api + Scenario: Seeing statistics for the current year and default channel when expectations are not specified + Given it is "last day of December last year" now + And 2 new customers have fulfilled 2 orders placed for total of "$1,000.00" + And it is "first day of January this year" now + And 3 new customers have fulfilled 4 orders placed for total of "$2,000.21" + And it is "first day of February this year" now + And 2 more new customers have paid 2 orders placed for total of "$5,000.37" + When I view statistics + Then I should see 5 new customers + And I should see 6 paid orders + And there should be total sales of "$7,000.58" + And the average order value should be "$1,166.76" + + @api @ui @javascript + Scenario: Seeing statistics for the previous year + Given it is "first day of January last year" now + And 3 new customers have fulfilled 2 orders placed for total of "$2,000.00" + And it is "first day of February this year" now + And 4 more new customers have paid 5 orders placed for total of "$5,000.37" + And 2 more new customers have paid 2 orders placed for total of "$5,000.37" + When I view statistics for "United States" channel and previous year split by month + Then I should see 3 new customers + And I should see 2 paid orders + And there should be total sales of "$2,000.00" + And the average order value should be "$1,000.00" + + @ui @javascript @no-api + Scenario: Seeing statistics for the next year + Given it is "first day of January last year" now + And 3 new customers have fulfilled 2 orders placed for total of "$2,000.00" + And it is "first day of February this year" now + And 4 more new customers have paid 5 orders placed for total of "$5,000.37" + And 2 more new customers have paid 2 orders placed for total of "$5,000.37" + When I view statistics for "United States" channel and previous year split by month + And I view statistics for "United States" channel and next year split by month + Then I should see 6 new customers + And I should see 7 paid orders + And there should be total sales of "$10,000.74" + And the average order value should be "$1,428.68" + + @api @ui @javascript + Scenario: Seeing statistics that include only fulfilled orders that were not cancelled + Given 4 new customers have fulfilled 4 orders placed for total of "$5,241.00" + And 2 more new customers have placed 2 orders for total of "$459.00" + And 2 new customers have added products to the cart for total of "$3,450.00" + And a single customer has placed an order for total of "$1,000.00" + But the customer cancelled this order + When I view statistics for "United States" channel and current year split by month + Then I should see 4 paid orders + And I should see 9 new customers + And there should be total sales of "$5,241.00" + And the average order value should be "$1,310.25" diff --git a/features/admin/statistics_per_channel.feature b/features/admin/statistics_per_channel.feature new file mode 100644 index 0000000000..6544f3ccdd --- /dev/null +++ b/features/admin/statistics_per_channel.feature @@ -0,0 +1,62 @@ +@admin_dashboard +Feature: Statistics for a specific channel + In order to gain insight into my sales performance and customers activity in a specific channel + As an Administrator + I want to view comprehensive statistics + + Background: + Given the store operates on a channel named "WEB-POLAND" + And there is product "Onion" available in this channel + And the store operates on another channel named "WEB-US" + And there is product "Banana" available in that channel + And the store ships everywhere for Free + And the store allows paying Offline + And I am logged in as an administrator + + @ui @no-api + Scenario: Seeing basic statistics for the first channel by default + Given 3 new customers have fulfilled 4 orders placed for total of "$8,566.00" mostly "Onion" product + And 2 more new customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product + When I view statistics + Then I should see 4 paid orders + And I should see 5 new customers + And there should be total sales of "$8,566.00" + And the average order value should be "$2,141.50" + + @ui @mink:chromedriver @api + Scenario: Switching to the channel with only fulfilled orders + Given 4 new customers have fulfilled 4 orders placed for total of "$5,241.00" mostly "Onion" product + And 2 more new customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product + And 2 more new customers have placed 3 orders for total of "$1,259.00" mostly "Banana" product + When I view statistics for "WEB-POLAND" channel and current year split by month + And I choose "WEB-US" channel + Then I should see 2 paid orders + And I should see 8 new customers + And there should be total sales of "$459.00" + And the average order value should be "$229.50" + + @ui @mink:chromedriver @api + Scenario: Switching to the channel with both fulfilled and placed orders + Given 4 new customers have fulfilled 4 orders placed for total of "$5,241.00" mostly "Onion" product + And 2 more new customers have fulfilled 2 orders placed for total of "$459.00" mostly "Banana" product + And 2 more new customers have placed 3 orders for total of "$1,259.00" mostly "Banana" product + When I view statistics for "WEB-US" channel + And I choose "WEB-POLAND" channel + Then I should see 4 paid orders + And I should see 8 new customers + And there should be total sales of "$5,241.00" + And the average order value should be "$1,310.25" + + @no-api @ui + Scenario: Seeing recent orders in a specific channel + Given 3 new customers have placed 4 orders for total of "$8,566.00" mostly "Onion" product + And 2 more new customers have placed 2 orders for total of "$459.00" mostly "Banana" product + When I view statistics for "WEB-POLAND" channel + Then I should see 4 new orders in the list + + @no-api @ui + Scenario: Seeing recent orders in a specific channel + Given 3 new customers have placed 4 orders for total of "$8,566.00" mostly "Onion" product + And 2 more new customers have placed 2 orders for total of "$459.00" mostly "Banana" product + When I view statistics for "WEB-US" channel + Then I should see 2 new orders in the list diff --git a/features/cart/shopping_cart/adding_product_with_discounted_catalog_price_to_cart.feature b/features/cart/shopping_cart/adding_product_with_discounted_catalog_price_to_cart.feature index 7f05607815..a5b84be080 100644 --- a/features/cart/shopping_cart/adding_product_with_discounted_catalog_price_to_cart.feature +++ b/features/cart/shopping_cart/adding_product_with_discounted_catalog_price_to_cart.feature @@ -6,8 +6,8 @@ Feature: Adding a simple product with discounted catalog price to the cart Background: Given the store operates on a single channel in "United States" - And the store has a product "Mug" priced at "$40" - And the store has a product "T-Shirt" priced at "$20" + And the store has a product "Mug" priced at "$40.00" + And the store has a product "T-Shirt" priced at "$20.00" And there is a catalog promotion "Winter sale" that reduces price by "25%" and applies on "T-Shirt" variant @ui @api @@ -23,7 +23,7 @@ Feature: Adding a simple product with discounted catalog price to the cart @ui @api Scenario: Adding a simple product with catalog and cart promotion to the cart Given there is a promotion "Cheap Stuff" - And this promotion gives "50%" off on every product when the item total is at least "$5" + And this promotion gives "50%" off on every product when the item total is at least "$5.00" When I add product "T-Shirt" to the cart And I add product "Mug" to the cart Then I should be on my cart summary page diff --git a/features/cart/shopping_cart/adding_product_with_options_to_cart.feature b/features/cart/shopping_cart/adding_product_with_options_to_cart.feature index 4b9171bdc9..57aeddf048 100644 --- a/features/cart/shopping_cart/adding_product_with_options_to_cart.feature +++ b/features/cart/shopping_cart/adding_product_with_options_to_cart.feature @@ -6,12 +6,12 @@ Feature: Adding a product with selected option to the cart Background: Given the store operates on a single channel in "United States" + And the store has a product "T-Shirt banana" + And this product has option "Size" with values "S" and "M" + And this product has all possible variants @ui @api Scenario: Adding a product with single option to the cart - Given the store has a product "T-Shirt banana" - And this product has option "Size" with values "S" and "M" - And this product has all possible variants When I add "T-Shirt banana" with Size "M" to the cart Then I should be on my cart summary page And I should be notified that the product has been successfully added diff --git a/features/cart/shopping_cart/adding_product_with_variants_with_discounted_catalog_price_to_cart.feature b/features/cart/shopping_cart/adding_product_with_variants_with_discounted_catalog_price_to_cart.feature index 50e16b42b3..17a1fa34ae 100644 --- a/features/cart/shopping_cart/adding_product_with_variants_with_discounted_catalog_price_to_cart.feature +++ b/features/cart/shopping_cart/adding_product_with_variants_with_discounted_catalog_price_to_cart.feature @@ -7,11 +7,11 @@ Feature: Adding a product with selected variant with discounted catalog price to Background: Given the store operates on a single channel in "United States" And the store has a "T-Shirt" configurable product - And the product "T-Shirt" has a "PHP T-Shirt" variant priced at "$20" - And the product "T-Shirt" has a "Kotlin T-Shirt" variant priced at "$400" + And the product "T-Shirt" has a "PHP T-Shirt" variant priced at "$20.00" + And the product "T-Shirt" has a "Kotlin T-Shirt" variant priced at "$400.00" And the store has a "Keyboard" configurable product - And the product "Keyboard" has a "RGB Keyboard" variant priced at "$40" - And the product "Keyboard" has a "Pink Keyboard" variant priced at "$40" + And the product "Keyboard" has a "RGB Keyboard" variant priced at "$40.00" + And the product "Keyboard" has a "Pink Keyboard" variant priced at "$40.00" And there is a catalog promotion "Winter sale" that reduces price by "25%" and applies on "PHP T-Shirt" variant @ui @api diff --git a/features/cart/shopping_cart/allowing_access_only_for_correctly_logged_in_users.feature b/features/cart/shopping_cart/allowing_access_only_for_correctly_logged_in_users.feature index 5b313e8daf..383294f4b2 100644 --- a/features/cart/shopping_cart/allowing_access_only_for_correctly_logged_in_users.feature +++ b/features/cart/shopping_cart/allowing_access_only_for_correctly_logged_in_users.feature @@ -172,20 +172,3 @@ Feature: Allowing access only for correctly logged in users And the customer has product "Stark T-Shirt" in the cart And the customer logged out Then the visitor has no access to change product "Stark T-Shirt" quantity to 2 in the customer cart - - @api - Scenario: Accessing to the customers cart by the admin - Given the customer logged in - And the customer has product "Stark T-Shirt" in the cart - And the customer logged out - And there is logged in the administrator - When the administrator try to see the summary of customer's cart - Then the administrator should see "Stark T-Shirt" product with quantity 1 in the customer cart - - @api - Scenario: Accessing to the visitors cart by the admin - Given there is the visitor - And the visitor has product "Stark T-Shirt" in the cart - And there is logged in the administrator - When the administrator try to see the summary of customer's cart - Then the administrator should see "Stark T-Shirt" product with quantity 1 in the visitor cart diff --git a/features/cart/shopping_cart/clearing_cart_after_logging_out.feature b/features/cart/shopping_cart/clearing_cart_after_logging_out.feature index 589c83b968..459748448e 100644 --- a/features/cart/shopping_cart/clearing_cart_after_logging_out.feature +++ b/features/cart/shopping_cart/clearing_cart_after_logging_out.feature @@ -8,7 +8,7 @@ Feature: Clearing cart after logging out Given the store operates on a single channel in "United States" And the store has a product "Stark T-Shirt" priced at "$12.00" - @ui + @ui @no-api Scenario: Clearing cart after logging out Given I am a logged in customer And I have product "Stark T-Shirt" in the cart @@ -16,14 +16,14 @@ Feature: Clearing cart after logging out And I see the summary of my cart Then my cart should be empty - @api + @api @no-ui Scenario: Clearing cart after logging out Given I am a logged in customer And I have product "Stark T-Shirt" in the cart When I log out Then I should not have access to the summary of my previous cart - @api + @api @no-ui Scenario: Blocking access to cart if logged user did any action over it (what can be treated as signing it) Given there is a user "john@snow.com" When I add this product to the cart diff --git a/features/cart/shopping_cart/resolving_default_shipping_method_in_cart_summary_based_on_method_position.feature b/features/cart/shopping_cart/resolving_default_shipping_method_in_cart_summary_based_on_method_position.feature index 81b296e8af..ee2e8f31de 100644 --- a/features/cart/shopping_cart/resolving_default_shipping_method_in_cart_summary_based_on_method_position.feature +++ b/features/cart/shopping_cart/resolving_default_shipping_method_in_cart_summary_based_on_method_position.feature @@ -8,7 +8,7 @@ Feature: Viewing a cart summary with the correct default shipping method Given the store operates on a single channel in "United States" And the store allows shipping with "Method 1" at position 2 with "$5.00" fee And the store also allows shipping with "Method 2" at position 0 with "$6.00" fee - And the store has a product "T-Shirt banana" priced at "$10" + And the store has a product "T-Shirt banana" priced at "$10.00" @ui @api Scenario: diff --git a/features/channel/managing_channels/adding_channel.feature b/features/channel/managing_channels/adding_channel.feature index 30b6d2ef91..32265b9a97 100644 --- a/features/channel/managing_channels/adding_channel.feature +++ b/features/channel/managing_channels/adding_channel.feature @@ -10,19 +10,20 @@ Feature: Adding a new channel And the store operates in "United States" and "Poland" And I am logged in as an administrator - @ui @api + @api @ui Scenario: Adding a new channel When I want to create a new channel And I specify its code as "MOBILE" And I name it "Mobile channel" And I choose "Euro" as the base currency + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale And I select the "Order items based" as tax calculation strategy And I add it Then I should be notified that it has been successfully created And the channel "Mobile channel" should appear in the registry - @ui @api + @api @ui Scenario: Adding a new channel with additional fields When I want to create a new channel And I specify its code as "MOBILE" @@ -33,6 +34,7 @@ Feature: Adding a new channel And I set its contact phone number as "11331122" And I define its color as "blue" And I choose "Euro" as the base currency + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale And I choose "United States" and "Poland" as operating countries And I select the "Order items based" as tax calculation strategy diff --git a/features/channel/managing_channels/adding_channel_with_set_period_of_showing_lowest_price.feature b/features/channel/managing_channels/adding_channel_with_set_period_of_showing_lowest_price.feature new file mode 100644 index 0000000000..d4b7719563 --- /dev/null +++ b/features/channel/managing_channels/adding_channel_with_set_period_of_showing_lowest_price.feature @@ -0,0 +1,79 @@ +@managing_channels +Feature: Specifying the lowest price for discounted products checking period while creating a channel + In order to show lowest prices only from a specific period + As an Administrator + I want to add a new channel with the lowest price for discounted products checking period + + Background: + Given the store has currency "Euro" + And the store has locale "English (United States)" + And the store operates in "United States" and "Poland" + And I am logged in as an administrator + + @api @ui + Scenario: Adding a new channel without specifying the lowest price for discounted products checking period + When I want to create a new channel + And I specify its code as "MOBILE" + And I name it "Mobile" + And I choose "Euro" as the base currency + And I make it available in "English (United States)" + And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy + And I add it + Then I should be notified that it has been successfully created + And the "Mobile" channel should have the lowest price for discounted products checking period set to 30 days + + @api @ui + Scenario: Adding a new channel with a specified lowest price for discounted products checking period + When I want to create a new channel + And I specify its code as "MOBILE" + And I name it "Mobile" + And I choose "Euro" as the base currency + And I make it available in "English (United States)" + And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy + And I specify 15 days as the lowest price for discounted products checking period + And I add it + Then I should be notified that it has been successfully created + And the "Mobile" channel should have the lowest price for discounted products checking period set to 15 days + + @api @ui + Scenario: Being prevented from creating a new channel with the lowest price for discounted products checking period equal to zero + When I want to create a new channel + And I specify its code as "MOBILE" + And I name it "Mobile" + And I choose "Euro" as the base currency + And I make it available in "English (United States)" + And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy + And I specify 0 days as the lowest price for discounted products checking period + And I try to add it + Then I should be notified that the lowest price for discounted products checking period must be greater than 0 + + @api @ui + Scenario: Being prevented from creating a new channel with a negative lowest price for discounted products checking period + When I want to create a new channel + And I specify its code as "MOBILE" + And I name it "Mobile" + And I choose "Euro" as the base currency + And I make it available in "English (United States)" + And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy + And I disable showing the lowest price of discounted products + And I specify -10 days as the lowest price for discounted products checking period + And I try to add it + Then I should be notified that the lowest price for discounted products checking period must be greater than 0 + + @api @ui + Scenario: Being prevented from creating a new channel with a too big lowest price for discounted products checking period + When I want to create a new channel + And I specify its code as "MOBILE" + And I name it "Mobile" + And I choose "Euro" as the base currency + And I make it available in "English (United States)" + And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy + And I disable showing the lowest price of discounted products + And I specify 99999999999 days as the lowest price for discounted products checking period + And I try to add it + Then I should be notified that the lowest price for discounted products checking period must be lower diff --git a/features/channel/managing_channels/adding_channel_with_shop_billing_data.feature b/features/channel/managing_channels/adding_channel_with_shop_billing_data.feature index 6c6ea0eee4..cb405a8382 100644 --- a/features/channel/managing_channels/adding_channel_with_shop_billing_data.feature +++ b/features/channel/managing_channels/adding_channel_with_shop_billing_data.feature @@ -10,12 +10,13 @@ Feature: Adding a new channel with shop billing data And the store operates in "United States" And I am logged in as an administrator - @ui @api + @api @ui Scenario: Adding a new channel with shop billing data When I want to create a new channel And I specify its code as "MOBILE" And I name it "Mobile channel" And I choose "Euro" as the base currency + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale And I select the "Order items based" as tax calculation strategy And I specify company as "Ragnarok" diff --git a/features/channel/managing_channels/adding_channel_with_toggle_for_lowest_price.feature b/features/channel/managing_channels/adding_channel_with_toggle_for_lowest_price.feature new file mode 100644 index 0000000000..27e47fec42 --- /dev/null +++ b/features/channel/managing_channels/adding_channel_with_toggle_for_lowest_price.feature @@ -0,0 +1,52 @@ +@managing_channels +Feature: Choosing whether to show the lowest product price or not while creating a channel + In order to show the lowest price before the product has been discounted only for certain channels + As an Administrator + I want to add a new channel that has this feature enabled or not + + Background: + Given the store has currency "Euro" + And the store has locale "English (United States)" + And the store operates in "United States" and "Poland" + And I am logged in as an administrator + + @api @ui + Scenario: Adding a new channel with lowest price before the product has been discounted enabled by default + When I want to create a new channel + And I specify its code as "MOBILE" + And I name it "Mobile" + And I choose "Euro" as the base currency + And I make it available in "English (United States)" + And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy + And I add it + Then I should be notified that it has been successfully created + And the "Mobile" channel should have the lowest price of discounted products prior to the current discount enabled + + @api @ui + Scenario: Adding a new channel with lowest price before the product has been discounted enabled + When I want to create a new channel + And I specify its code as "MOBILE" + And I name it "Mobile" + And I choose "Euro" as the base currency + And I make it available in "English (United States)" + And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy + And I enable showing the lowest price of discounted products + And I add it + Then I should be notified that it has been successfully created + And the "Mobile" channel should have the lowest price of discounted products prior to the current discount enabled + + @api @ui + Scenario: Adding a new channel with lowest price before the product has been discounted disabled + When I want to create a new channel + And I specify its code as "MOBILE" + And I name it "Mobile" + And I choose "Euro" as the base currency + And I make it available in "English (United States)" + And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy + And I disable showing the lowest price of discounted products + And I add it + Then I should be notified that it has been successfully created + And the "Mobile" channel should have the lowest price of discounted products prior to the current discount disabled diff --git a/features/channel/managing_channels/adding_new_channel_with_menu_taxon.feature b/features/channel/managing_channels/adding_new_channel_with_menu_taxon.feature index 943ae8e4aa..899fb6c75f 100644 --- a/features/channel/managing_channels/adding_new_channel_with_menu_taxon.feature +++ b/features/channel/managing_channels/adding_new_channel_with_menu_taxon.feature @@ -17,6 +17,7 @@ Feature: Adding a new channel with menu taxon And I specify its code as "MOBILE" And I name it "Mobile channel" And I choose "Euro" as the base currency + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale And I select the "Order items based" as tax calculation strategy And I specify menu taxon as "Clothes" diff --git a/features/channel/managing_channels/browsing_channels.feature b/features/channel/managing_channels/browsing_channels.feature index f95b116ae0..070993551b 100644 --- a/features/channel/managing_channels/browsing_channels.feature +++ b/features/channel/managing_channels/browsing_channels.feature @@ -9,7 +9,7 @@ Feature: Browsing channels And the store operates on another channel named "Mobile Channel" And I am logged in as an administrator - @ui @api + @api @ui Scenario: Browsing defined channels When I want to browse channels Then I should see 2 channels in the list diff --git a/features/channel/managing_channels/channel_unique_code_validation.feature b/features/channel/managing_channels/channel_unique_code_validation.feature index f8b6e62cff..47d6543623 100644 --- a/features/channel/managing_channels/channel_unique_code_validation.feature +++ b/features/channel/managing_channels/channel_unique_code_validation.feature @@ -8,7 +8,7 @@ Feature: Channel unique code validation Given the store operates on a channel identified by "WEB" code And I am logged in as an administrator - @ui + @api @ui Scenario: Trying to add channel with taken code When I want to create a new channel And I specify its code as "WEB" diff --git a/features/channel/managing_channels/channel_validation.feature b/features/channel/managing_channels/channel_validation.feature index fe45e18b4b..766a3a4a2c 100644 --- a/features/channel/managing_channels/channel_validation.feature +++ b/features/channel/managing_channels/channel_validation.feature @@ -7,7 +7,7 @@ Feature: Channel validation Background: Given I am logged in as an administrator - @ui + @api @ui Scenario: Trying to add a new channel without specifying its code When I want to create a new channel And I name it "Mobile channel" @@ -16,7 +16,7 @@ Feature: Channel validation Then I should be notified that code is required And channel with name "Mobile channel" should not be added - @ui + @api @ui Scenario: Trying to add a new channel without specifying its name When I want to create a new channel And I specify its code as "MOBILE" @@ -25,7 +25,7 @@ Feature: Channel validation Then I should be notified that name is required And channel with code "MOBILE" should not be added - @ui + @api @ui Scenario: Trying to add a new channel without base currency When I want to create a new channel And I specify its code as "MOBILE" @@ -34,7 +34,7 @@ Feature: Channel validation Then I should be notified that base currency is required And channel with code "MOBILE" should not be added - @ui + @api @ui Scenario: Trying to add a new channel without default locale When I want to create a new channel And I specify its code as "MOBILE" @@ -43,7 +43,7 @@ Feature: Channel validation Then I should be notified that default locale is required And channel with code "MOBILE" should not be added - @ui + @api @ui Scenario: Trying to remove name from existing channel Given the store operates on a channel named "Web Channel" When I want to modify this channel diff --git a/features/channel/managing_channels/choosing_required_address_in_checkout_for_channel.feature b/features/channel/managing_channels/choosing_required_address_in_checkout_for_channel.feature index 718788f7ce..bd44a3db81 100644 --- a/features/channel/managing_channels/choosing_required_address_in_checkout_for_channel.feature +++ b/features/channel/managing_channels/choosing_required_address_in_checkout_for_channel.feature @@ -14,6 +14,7 @@ Feature: Choosing a required address in the checkout for a channel And I specify its code as "MOBILE" And I name it "Mobile Store" And I choose "USD" as the base currency + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale And I select the "Order items based" as tax calculation strategy And I choose shipping address as a required address in the checkout diff --git a/features/channel/managing_channels/deleting_channel.feature b/features/channel/managing_channels/deleting_channel.feature index 372e70c0eb..fb2976984b 100644 --- a/features/channel/managing_channels/deleting_channel.feature +++ b/features/channel/managing_channels/deleting_channel.feature @@ -9,7 +9,7 @@ Feature: Deleting a channel And the store operates on another channel named "Mobile Store" And I am logged in as an administrator - @ui + @api @ui Scenario: Deleted channel should disappear from the registry When I delete channel "Web Store" Then I should be notified that it has been successfully deleted diff --git a/features/channel/managing_channels/deleting_multiple_channels.feature b/features/channel/managing_channels/deleting_multiple_channels.feature index 4b0ae533a2..5346dc8041 100644 --- a/features/channel/managing_channels/deleting_multiple_channels.feature +++ b/features/channel/managing_channels/deleting_multiple_channels.feature @@ -10,7 +10,7 @@ Feature: Deleting multiple channels And the store operates on another channel named "DE Store" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple channels at once When I browse channels And I check the "PL Store" channel diff --git a/features/channel/managing_channels/editing_channel.feature b/features/channel/managing_channels/editing_channel.feature index 4a011b1f75..42e7fa0046 100644 --- a/features/channel/managing_channels/editing_channel.feature +++ b/features/channel/managing_channels/editing_channel.feature @@ -8,20 +8,12 @@ Feature: Editing channel Given the store operates on a channel named "Web Channel" And I am logged in as an administrator - @todo - Scenario: Trying to change channel code + @api @ui + Scenario: Being unable to change the code of an existing channel When I want to modify a channel "Web Channel" - And I change its code to "MOBILE" - And I save my changes - Then I should be notified that code cannot be changed - And channel "Web Channel" should still have code "MOBILE" + Then I should not be able to edit its code - @ui - Scenario: Seeing disabled code field when editing channel - When I want to modify a channel "Web Channel" - Then the code field should be disabled - - @ui + @api @ui Scenario: Renaming the channel When I want to modify a channel "Web Channel" And I rename it to "Website store" @@ -29,7 +21,7 @@ Feature: Editing channel Then I should be notified that it has been successfully edited And this channel name should be "Website store" - @ui - Scenario: Seeing disabled base currency field during channel edition + @api @ui + Scenario: Being unable to change base currency of an existing channel When I want to modify a channel "Web Channel" - Then the base currency field should be disabled + Then I should not be able to edit its base currency diff --git a/features/channel/managing_channels/editing_channel_with_set_period_of_showing_lowest_price.feature b/features/channel/managing_channels/editing_channel_with_set_period_of_showing_lowest_price.feature new file mode 100644 index 0000000000..0e895a2f45 --- /dev/null +++ b/features/channel/managing_channels/editing_channel_with_set_period_of_showing_lowest_price.feature @@ -0,0 +1,68 @@ +@managing_channels +Feature: Specifying the lowest price for discounted products checking period while editing a channel + In order to show lowest prices only from a specific period + As an Administrator + I want to edit a channel's lowest price for discounted products checking period + + Background: + Given the store operates on a channel named "EU" + And this channel has 15 days set as the lowest price for discounted products checking period + And I am logged in as an administrator + + @no-api @ui + Scenario: Changing the lowest price for discounted products checking period + When I want to modify a channel "EU" + And I specify 30 days as the lowest price for discounted products checking period + And I save my changes + Then I should be notified that it has been successfully edited + And its lowest price for discounted products checking period should be set to 30 days + + @no-api @ui + Scenario: Being prevented from changing the lowest price for discounted products checking period to zero + When I want to modify a channel "EU" + And I specify 0 days as the lowest price for discounted products checking period + And I try to save my changes + Then I should be notified that the lowest price for discounted products checking period must be greater than 0 + + @no-api @ui + Scenario: Being prevented from changing the lowest price for discounted products checking period to a negative value + When I want to modify a channel "EU" + And I specify -10 days as the lowest price for discounted products checking period + And I try to save my changes + Then I should be notified that the lowest price for discounted products checking period must be greater than 0 + + @no-api @ui + Scenario: Being prevented from changing the lowest price for discounted products checking period to a too big value + When I want to modify a channel "EU" + And I specify 99999999999 days as the lowest price for discounted products checking period + And I try to save my changes + Then I should be notified that the lowest price for discounted products checking period must be lower + + @api @no-ui + Scenario: Changing the lowest price for discounted products checking period + When I want to modify the price history config of channel "EU" + And I change the lowest price for discounted products checking period to 30 days + And I save my changes + Then I should be notified that it has been successfully edited + And its lowest price for discounted products checking period should be set to 30 days + + @api @no-ui + Scenario: Being prevented from changing the lowest price for discounted products checking period to zero + When I want to modify the price history config of channel "EU" + And I change the lowest price for discounted products checking period to 0 days + And I try to save my changes + Then I should be notified that the lowest price for discounted products checking period must be greater than 0 + + @api @no-ui + Scenario: Being prevented from changing the lowest price for discounted products checking period to a negative value + When I want to modify the price history config of channel "EU" + And I change the lowest price for discounted products checking period to -10 days + And I try to save my changes + Then I should be notified that the lowest price for discounted products checking period must be greater than 0 + + @api @no-ui + Scenario: Being prevented from changing the lowest price for discounted products checking period to a too big value + When I want to modify the price history config of channel "EU" + And I change the lowest price for discounted products checking period to 99999999999 days + And I try to save my changes + Then I should be notified that the lowest price for discounted products checking period must be lower diff --git a/features/channel/managing_channels/editing_channel_with_toggle_for_lowest_price.feature b/features/channel/managing_channels/editing_channel_with_toggle_for_lowest_price.feature new file mode 100644 index 0000000000..e24f38ca61 --- /dev/null +++ b/features/channel/managing_channels/editing_channel_with_toggle_for_lowest_price.feature @@ -0,0 +1,44 @@ +@managing_channels +Feature: Choosing whether to show the lowest product price or not while editing a channel + In order to show the lowest price before the product has been discounted only for certain channels + As an Administrator + I want to be able to edit channels and enable or disable the lowest price of discounted products on them + + Background: + Given the store operates on a channel named "EU Channel" + And the store operates on another channel named "US Channel" + And the channel "EU Channel" has showing the lowest price of discounted products enabled + And the channel "US Channel" has showing the lowest price of discounted products disabled + And I am logged in as an administrator + + @no-api @ui + Scenario: Enabling showing the lowest price of discounted products on a channel + When I want to modify a channel "US Channel" + And I enable showing the lowest price of discounted products + And I save my changes + Then I should be notified that it has been successfully edited + And the "US Channel" channel should have the lowest price of discounted products prior to the current discount enabled + + @no-api @ui + Scenario: Disabling showing the lowest price of discounted products on a channel + When I want to modify a channel "EU Channel" + And I disable showing the lowest price of discounted products + And I save my changes + Then I should be notified that it has been successfully edited + And the "EU Channel" channel should have the lowest price of discounted products prior to the current discount disabled + + @api @no-ui + Scenario: Enabling showing the lowest price of discounted products on a channel + When I want to modify the price history config of channel "US Channel" + And I change showing of the lowest price of discounted products to be enabled + And I save my changes + Then I should be notified that it has been successfully edited + And the "US Channel" channel should have the lowest price of discounted products prior to the current discount enabled + + @api @no-ui + Scenario: Disabling showing the lowest price of discounted products on a channel + When I want to modify the price history config of channel "EU Channel" + And I change showing of the lowest price of discounted products to be disabled + And I save my changes + Then I should be notified that it has been successfully edited + And the "EU Channel" channel should have the lowest price of discounted products prior to the current discount disabled diff --git a/features/channel/managing_channels/editing_menu_taxon_on_channel.feature b/features/channel/managing_channels/editing_menu_taxon_on_channel.feature index bb32cbf8b5..e0440d57a1 100644 --- a/features/channel/managing_channels/editing_menu_taxon_on_channel.feature +++ b/features/channel/managing_channels/editing_menu_taxon_on_channel.feature @@ -11,7 +11,7 @@ Feature: Editing menu taxon on channel And channel "Web Store" has menu taxon "Clothes" And I am logged in as an administrator - @ui @javascript + @api @ui @javascript Scenario: Editing menu taxon on the channel When I want to modify a channel "Web Store" And I change its menu taxon to "Guns" diff --git a/features/channel/managing_channels/editing_shop_billing_data_on_channel.feature b/features/channel/managing_channels/editing_shop_billing_data_on_channel.feature index 801ae49a6e..d1bb24601a 100644 --- a/features/channel/managing_channels/editing_shop_billing_data_on_channel.feature +++ b/features/channel/managing_channels/editing_shop_billing_data_on_channel.feature @@ -10,14 +10,20 @@ Feature: Editing shop billing data on channel And channel "Web Store" billing data is "Ragnarok", "Pacific Coast Hwy", "90806" "Los Angeles", "United States" with "1100110011" tax ID And I am logged in as an administrator - @ui + @api @ui Scenario: Editing shop billing data on channel When I want to modify a channel "Web Store" - And I specify company as "Götterdämmerung" - And I specify tax ID as "666777" - And I specify shop billing address as "Valhalla", "123" "Asgard", "United States" + And I specify shop billing data for this channel as "Götterdämmerung", "Valhalla", "123", "Asgard", "666777" tax ID and "United States" country And I save my changes Then I should be notified that it has been successfully edited And this channel company should be "Götterdämmerung" And this channel tax ID should be "666777" - And this channel shop billing address should be "Valhalla", "123" "Asgard", "United States" + And this channel shop billing address should be "Valhalla", "123" "Asgard" and "United States" country + + @api @no-ui + Scenario: Editing shop billing data with wrong country code + When I want to modify a channel "Web Store" + And I specify new country code for this channel as "ZZ" + And I save my changes + Then I should be notified that it is not a valid country + And this channel shop billing address should still be "Pacific Coast Hwy", "90806" "Los Angeles" and "United States" country diff --git a/features/channel/managing_channels/excluding_chosen_taxons_from_showing_lowest_price_of_discounted_products.feature b/features/channel/managing_channels/excluding_chosen_taxons_from_showing_lowest_price_of_discounted_products.feature new file mode 100644 index 0000000000..60aa516949 --- /dev/null +++ b/features/channel/managing_channels/excluding_chosen_taxons_from_showing_lowest_price_of_discounted_products.feature @@ -0,0 +1,52 @@ +@managing_channels +Feature: Excluding chosen taxons from displaying the lowest price of discounted products + In order not to show the lowest price of discounted products on some taxons + As an Administrator + I want to be able to configure taxons for which the lowest price of discounted products is not displayed + + Background: + Given the store operates on a channel named "Poland" + And the store classifies its products as "T-Shirts", "Caps" and "Sweaters" + And I am logged in as an administrator + + @no-api @ui @javascript + Scenario: Excluding a singular taxon from displaying the lowest price of discounted products + When I want to modify a channel "Poland" + And I exclude the "T-Shirts" taxon from showing the lowest price of discounted products + And I save my changes + Then I should be notified that it has been successfully edited + And this channel should have "T-Shirts" taxon excluded from displaying the lowest price of discounted products + + @no-api @ui @mink:chromedriver + Scenario: Excluding multiple taxons from displaying the lowest price of discounted products + When I want to modify a channel "Poland" + And I exclude the "T-Shirts" and "Caps" taxons from showing the lowest price of discounted products + And I save my changes + Then I should be notified that it has been successfully edited + And this channel should have "T-Shirts" and "Caps" taxons excluded from displaying the lowest price of discounted products + + @no-api @ui @mink:chromedriver + Scenario: Removing excluded taxon from displaying the lowest price of discounted products + Given the channel "Poland" has "T-Shirts" and "Caps" taxons excluded from showing the lowest price of discounted products + When I want to modify this channel + And I remove the "T-Shirts" taxon from excluded taxons from showing the lowest price of discounted products + And I save my changes + Then I should be notified that it has been successfully edited + And this channel should have "Caps" taxon excluded from displaying the lowest price of discounted products + And this channel should not have "T-Shirts" taxon excluded from displaying the lowest price of discounted products + + @api @no-ui + Scenario: Excluding a singular taxon from displaying the lowest price of discounted products + When I want to modify the price history config of channel "Poland" + And I exclude the "T-Shirts" taxon from showing the lowest price of discounted products + And I save my changes + Then I should be notified that it has been successfully edited + And this channel should have "T-Shirts" taxon excluded from displaying the lowest price of discounted products + + @api @no-ui + Scenario: Excluding multiple taxons from displaying the lowest price of discounted products + When I want to modify the price history config of channel "Poland" + And I exclude the "T-Shirts" and "Caps" taxons from showing the lowest price of discounted products + And I save my changes + Then I should be notified that it has been successfully edited + And this channel should have "T-Shirts" and "Caps" taxons excluded from displaying the lowest price of discounted products diff --git a/features/channel/managing_channels/not_being_able_to_add_disabled_channel_when_no_other_exist.feature b/features/channel/managing_channels/not_being_able_to_add_disabled_channel_when_no_other_exist.feature index c9d19fd580..31f2cb450e 100644 --- a/features/channel/managing_channels/not_being_able_to_add_disabled_channel_when_no_other_exist.feature +++ b/features/channel/managing_channels/not_being_able_to_add_disabled_channel_when_no_other_exist.feature @@ -9,8 +9,8 @@ Feature: Not being able to add a disabled channel when no other exist And the store has locale "English (United States)" And I am logged in as an administrator - @ui - Scenario: Adding a new disabled channel should result + @api @ui + Scenario: Trying to add a new disabled channel when no other exist When I want to create a new channel And I specify its code as "MOBILE" And I name it "Mobile channel" diff --git a/features/channel/managing_channels/not_being_able_to_delete_last_available_channel.feature b/features/channel/managing_channels/not_being_able_to_delete_last_available_channel.feature index e30a65d27c..f46c66cb40 100644 --- a/features/channel/managing_channels/not_being_able_to_delete_last_available_channel.feature +++ b/features/channel/managing_channels/not_being_able_to_delete_last_available_channel.feature @@ -8,8 +8,8 @@ Feature: Not being able to delete a last available channel Given the store operates on a channel named "Web Store" And I am logged in as an administrator - @ui - Scenario: Prevented from deleting only channel + @api @ui + Scenario: Preventing from deleting only channel When I delete channel "Web Store" Then I should be notified that it cannot be deleted And this channel should still be in the registry diff --git a/features/channel/managing_channels/not_being_able_to_disable_last_available_channel.feature b/features/channel/managing_channels/not_being_able_to_disable_last_available_channel.feature index bb95ad1290..b7e302c4b4 100644 --- a/features/channel/managing_channels/not_being_able_to_disable_last_available_channel.feature +++ b/features/channel/managing_channels/not_being_able_to_disable_last_available_channel.feature @@ -8,7 +8,7 @@ Feature: Toggling a channel Given the store operates on a channel named "Web Channel" And I am logged in as an administrator - @ui + @api @ui Scenario: Disabling the last available channel Given the channel "Web Channel" is enabled When I want to modify this channel diff --git a/features/channel/managing_channels/selecting_currencies_available_for_channel.feature b/features/channel/managing_channels/selecting_currencies_available_for_channel.feature index b478e47268..339b63a728 100644 --- a/features/channel/managing_channels/selecting_currencies_available_for_channel.feature +++ b/features/channel/managing_channels/selecting_currencies_available_for_channel.feature @@ -7,20 +7,24 @@ Feature: Selecting available currencies for a channel Background: Given the store has currency "Euro" And the store has locale "English (United States)" + And the store operates in "United States" And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new channel with currencies When I want to create a new channel And I specify its code as MOBILE + And I choose "Euro" as the base currency And I name it "Mobile store" And I allow for paying in "Euro" + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy And I add it Then I should be notified that it has been successfully created And paying in Euro should be possible for the "Mobile store" channel - @ui + @api @ui Scenario: Adding currencies to an existing channel Given the store operates on a channel named "Web store" When I want to modify this channel diff --git a/features/channel/managing_channels/selecting_default_tax_zone_for_channel.feature b/features/channel/managing_channels/selecting_default_tax_zone_for_channel.feature index 891dcd70ae..bc3e2ef5db 100644 --- a/features/channel/managing_channels/selecting_default_tax_zone_for_channel.feature +++ b/features/channel/managing_channels/selecting_default_tax_zone_for_channel.feature @@ -8,19 +8,21 @@ Feature: Selecting default tax zone for a channel Given the store operates on a single channel in "United States" And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new channel with default tax zone When I want to create a new channel And I specify its code as "MOBILE" And I name it "Mobile store" And I select the "United States" as default tax zone And I choose "USD" as the base currency + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy And I add it Then I should be notified that it has been successfully created And the default tax zone for the "Mobile store" channel should be "United States" - @ui + @api @ui Scenario: Selecting default tax zone for existing channel Given the store operates on a channel named "Web store" When I want to modify this channel @@ -29,7 +31,7 @@ Feature: Selecting default tax zone for a channel Then I should be notified that it has been successfully edited And the default tax zone for the "Web store" channel should be "United States" - @ui + @api @ui Scenario: Removing existing channel default tax zone Given the store operates on a channel named "Web store" And its default tax zone is zone "US" diff --git a/features/channel/managing_channels/selecting_locales_available_for_channel.feature b/features/channel/managing_channels/selecting_locales_available_for_channel.feature index 25afdd3ff9..1c17675e3a 100644 --- a/features/channel/managing_channels/selecting_locales_available_for_channel.feature +++ b/features/channel/managing_channels/selecting_locales_available_for_channel.feature @@ -9,7 +9,7 @@ Feature: Selecting available locales for a channel And the store has locale "English (United States)" And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new channel with locales When I want to create a new channel And I specify its code as "MOBILE" @@ -17,11 +17,12 @@ Feature: Selecting available locales for a channel And I make it available in "English (United States)" And I choose "Euro" as the base currency And I choose "English (United States)" as a default locale + And I select the "Order items based" as tax calculation strategy And I add it Then I should be notified that it has been successfully created And the channel "Mobile Channel" should be available in "English (United States)" - @ui + @api @ui Scenario: Adding locales to an existing channel Given the store operates on a channel named "Web Channel" When I want to modify this channel @@ -30,23 +31,23 @@ Feature: Selecting available locales for a channel Then I should be notified that it has been successfully edited And the channel "Web Channel" should be available in "English (United States)" - @ui + @api @ui Scenario: Being unable to disable locale used as the default one for a channel Given the store operates on a channel named "Web" And this channel allows to shop using "English (United States)" and "Polish (Poland)" locales And this channel uses the "English (United States)" locale as default - And I am modifying a channel "Web" - When I make it available only in "Polish (Poland)" + When I want to modify this channel + And I make it available only in "Polish (Poland)" And I try to save my changes Then I should be notified that the default locale has to be enabled - @ui + @api @ui Scenario: Being unable to set disabled locale as a default one for a channel Given the store has locale "Polish (Poland)" And the store operates on a channel named "Web" And this channel allows to shop using the "English (United States)" locale And this channel uses the "English (United States)" locale as default - And I am modifying a channel "Web" - When I choose "Polish (Poland)" as a default locale + When I want to modify this channel + And I choose "Polish (Poland)" as a default locale And I try to save my changes Then I should be notified that the default locale has to be enabled diff --git a/features/channel/managing_channels/selecting_tax_calculation_strategy_for_channel.feature b/features/channel/managing_channels/selecting_tax_calculation_strategy_for_channel.feature index ff8f6bac6f..8b2d560df7 100644 --- a/features/channel/managing_channels/selecting_tax_calculation_strategy_for_channel.feature +++ b/features/channel/managing_channels/selecting_tax_calculation_strategy_for_channel.feature @@ -20,19 +20,20 @@ Feature: Selecting tax calculation strategy for a channel Then I should be notified that it has been successfully created And the tax calculation strategy for the "Mobile store" channel should be "Order items based" - @ui + @api @ui Scenario: Adding a new channel with tax calculation strategy When I want to create a new channel And I specify its code as "MOBILE" And I select the "Order item units based" as tax calculation strategy And I name it "Mobile store" And I choose "Euro" as the base currency + And I make it available in "English (United States)" And I choose "English (United States)" as a default locale And I add it Then I should be notified that it has been successfully created And the tax calculation strategy for the "Mobile store" channel should be "Order item units based" - @ui + @api @ui Scenario: Changing tax calculation strategy of existing channel Given the store operates on a channel named "Web store" When I want to modify this channel diff --git a/features/channel/managing_channels/toggling_channel.feature b/features/channel/managing_channels/toggling_channel.feature index 41875817f1..92d88bc4d0 100644 --- a/features/channel/managing_channels/toggling_channel.feature +++ b/features/channel/managing_channels/toggling_channel.feature @@ -9,7 +9,7 @@ Feature: Toggling a channel And the store operates on another channel named "Mobile Channel" And I am logged in as an administrator - @ui + @api @ui Scenario: Disabling the channel Given the channel "Web Channel" is enabled When I want to modify this channel @@ -18,7 +18,7 @@ Feature: Toggling a channel Then I should be notified that it has been successfully edited And this channel should be disabled - @ui + @api @ui Scenario: Enabling the channel Given the channel "Web Channel" is disabled When I want to modify this channel diff --git a/features/checkout/ability_to_change_email_during_checkout.feature b/features/checkout/ability_to_change_email_during_checkout.feature index 022db32b8d..fdaa9c0a65 100644 --- a/features/checkout/ability_to_change_email_during_checkout.feature +++ b/features/checkout/ability_to_change_email_during_checkout.feature @@ -6,7 +6,7 @@ Feature: Changing email during checkout with registered email Background: Given the store operates on a single channel in "United States" - And the store has a product "Mantis blade" priced at "$1200" + And the store has a product "Mantis blade" priced at "$1,200.00" And the store ships everywhere for Free And the store allows paying Offline And there is a customer "John Doe" identified by an email "john@example.com" and a password "secret" diff --git a/features/checkout/ability_to_checkout_as_guest_with_a_registered_email.feature b/features/checkout/ability_to_checkout_as_guest_with_a_registered_email.feature index dda9973e6c..a09fdf9c48 100644 --- a/features/checkout/ability_to_checkout_as_guest_with_a_registered_email.feature +++ b/features/checkout/ability_to_checkout_as_guest_with_a_registered_email.feature @@ -21,7 +21,7 @@ Feature: Checking out as guest with a registered email And I confirm my order Then I should see the thank you page - @ui + @ui @api Scenario: Placing an order using email with mixed case Given I have product "PHP T-Shirt" in the cart When I complete addressing step with email "JOhn@example.COM" and "United States" based billing address diff --git a/features/checkout/addressing_order/choosing_billing_and_shipping_address_from_address_book.feature b/features/checkout/addressing_order/choosing_billing_and_shipping_address_from_address_book.feature index 1e580a73ae..6bd9587ac6 100644 --- a/features/checkout/addressing_order/choosing_billing_and_shipping_address_from_address_book.feature +++ b/features/checkout/addressing_order/choosing_billing_and_shipping_address_from_address_book.feature @@ -15,7 +15,7 @@ Feature: Choosing an address from address book And I have an address "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States", "Arkansas" in my address book And I have an address "Fletcher Ren", "Upper Barkly Street", "3377", "Ararat", "Australia", "Victoria" in my address book - @ui @javascript @api + @ui @mink:chromedriver @api Scenario: Choosing billing address from address book Given I have product "PHP T-Shirt" in the cart And I am at the checkout addressing step @@ -36,7 +36,7 @@ Feature: Choosing an address from address book When I choose "Upper Barkly Street" street for billing address Then address "Fletcher Ren", "Upper Barkly Street", "3377", "Ararat", "Australia", "Victoria" should be filled as billing address - @ui @javascript @api + @ui @mink:chromedriver @api Scenario: Choosing billing address from address book and proceed to the next step Given I have product "PHP T-Shirt" in the cart And I am at the checkout addressing step diff --git a/features/checkout/addressing_order/choosing_province_for_country.feature b/features/checkout/addressing_order/choosing_province_for_country.feature index cc3852448a..6b98aaf26f 100644 --- a/features/checkout/addressing_order/choosing_province_for_country.feature +++ b/features/checkout/addressing_order/choosing_province_for_country.feature @@ -12,7 +12,7 @@ Feature: Choosing province for country And the store ships everywhere for Free And I am a logged in customer - @ui @javascript @api + @ui @mink:chromedriver @api Scenario: Address an order with country and its province Given I have product "The Dark Knight T-Shirt" in the cart And I am at the checkout addressing step @@ -21,7 +21,7 @@ Feature: Choosing province for country And I complete the addressing step Then I should be on the checkout shipping step - @ui @javascript @api + @ui @mink:chromedriver @api Scenario: Address an order with country and its province and specify country without province for different billing address Given I have product "The Dark Knight T-Shirt" in the cart And I am at the checkout addressing step @@ -31,7 +31,7 @@ Feature: Choosing province for country And I complete the addressing step Then I should be on the checkout shipping step - @ui @javascript @api + @ui @mink:chromedriver @api Scenario: Address an order with country and its province and specify country with province for different billing address Given I have product "The Dark Knight T-Shirt" in the cart And I am at the checkout addressing step diff --git a/features/checkout/addressing_order/filling_shipping_address_details_as_guest_with_existing_account.feature b/features/checkout/addressing_order/filling_shipping_address_details_as_guest_with_existing_account.feature index 3d09aeba4f..146b77f57b 100644 --- a/features/checkout/addressing_order/filling_shipping_address_details_as_guest_with_existing_account.feature +++ b/features/checkout/addressing_order/filling_shipping_address_details_as_guest_with_existing_account.feature @@ -10,7 +10,7 @@ Feature: Addressing an order and signing in And the store has a product "PHP T-Shirt" priced at "$19.99" And there is a customer "Francis Underwood" identified by an email "francis@underwood.com" and a password "whitehouse" - @ui @javascript + @ui @mink:chromedriver @no-api Scenario: Addressing an order and signing in Given I have product "PHP T-Shirt" in the cart And I am at the checkout addressing step diff --git a/features/checkout/addressing_order/returning_to_addressing_step_with_a_different_shipping_address.feature b/features/checkout/addressing_order/returning_to_addressing_step_with_a_different_shipping_address.feature index 54d4e3f89a..1a303e8e9b 100644 --- a/features/checkout/addressing_order/returning_to_addressing_step_with_a_different_shipping_address.feature +++ b/features/checkout/addressing_order/returning_to_addressing_step_with_a_different_shipping_address.feature @@ -30,7 +30,7 @@ Feature: Returning to addressing step with a different shipping address And I decide to change my address Then different shipping address should not be checked - @ui @no-api @javascript + @ui @no-api @mink:chromedriver Scenario: Going back to addressing step after submitting a different shipping address Given I have product "Summer T-Shirt" in the cart And I am at the checkout addressing step @@ -41,7 +41,7 @@ Feature: Returning to addressing step with a different shipping address And I decide to change my address And shipping address should be visible - @ui @no-api @javascript + @ui @no-api @mink:chromedriver Scenario: Going back to addressing step after not submitting a different shipping address Given I have product "Summer T-Shirt" in the cart And I am at the checkout addressing step diff --git a/features/checkout/order_promotion/order_promotion_integrity_validation.feature b/features/checkout/order_promotion/order_promotion_integrity_validation.feature index f73b72d25c..7a73ec035a 100644 --- a/features/checkout/order_promotion/order_promotion_integrity_validation.feature +++ b/features/checkout/order_promotion/order_promotion_integrity_validation.feature @@ -73,3 +73,14 @@ Feature: Order promotions integrity And I proceeded with "Free" shipping method and "Offline" payment method When I confirm my order Then I should see the thank you page + + @ui @api + Scenario: Preventing customer from completing checkout with already archived promotion + Given this promotion gives "$10.00" discount to every order + And I added product "PHP T-Shirt" to the cart + And I have specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" + And I proceeded with "Free" shipping method and "Offline" payment method + And the promotion "Christmas sale" is archived + When I try to confirm my order + Then I should be informed that this promotion is no longer applied + And I should not see the thank you page diff --git a/features/checkout/paying_for_order/accessing_order_right_after_completing_checkout.feature b/features/checkout/paying_for_order/accessing_order_right_after_completing_checkout.feature index fbcb44534c..d5c022b913 100644 --- a/features/checkout/paying_for_order/accessing_order_right_after_completing_checkout.feature +++ b/features/checkout/paying_for_order/accessing_order_right_after_completing_checkout.feature @@ -11,7 +11,7 @@ Feature: Having good number of items in changing payment method page And the store has a product "PHP T-Shirt" priced at "$19.99" And the store ships everywhere for Free - @ui + @ui @no-api Scenario: Seeing correct quantity on payment retry page Given I have added 2 products "PHP T-Shirt" to the cart And I complete addressing step with email "john@example.com" and "United States" based billing address diff --git a/features/checkout/paying_for_order/changing_offline_payment_method_after_order_confirmation.feature b/features/checkout/paying_for_order/changing_offline_payment_method_after_order_confirmation.feature index dd2feca1d3..af63dc8765 100644 --- a/features/checkout/paying_for_order/changing_offline_payment_method_after_order_confirmation.feature +++ b/features/checkout/paying_for_order/changing_offline_payment_method_after_order_confirmation.feature @@ -11,7 +11,7 @@ Feature: Changing the offline payment method after order confirmation And the store has a product "PHP T-Shirt" priced at "$19.99" And the store ships everywhere for Free - @ui + @ui @api Scenario: Retrying the payment with different Offline payment Given I added product "PHP T-Shirt" to the cart When I complete addressing step with email "john@example.com" and "United States" based billing address diff --git a/features/checkout/paying_for_order/inform_customer_about_order_total_changes.feature b/features/checkout/paying_for_order/inform_customer_about_order_total_changes.feature index 1ebb28321d..0550034b7f 100644 --- a/features/checkout/paying_for_order/inform_customer_about_order_total_changes.feature +++ b/features/checkout/paying_for_order/inform_customer_about_order_total_changes.feature @@ -12,37 +12,34 @@ Feature: Inform customer about any order total changes during checkout process And the store ships everywhere for Free And the store allows paying Offline - @ui + @ui @api Scenario: Inform customer about order total change due to product price change Given I am a logged in customer And I added product "PHP T-Shirt" to the cart - And I have proceeded selecting "Offline" payment method + And I proceeded through checkout process And this product price has been changed to "$25.00" When I confirm my order - Then I should be informed that order total has been changed - And I should not see the thank you page + Then my order should not be placed due to changed order total - @ui + @ui @api Scenario: Be able to confirm order after information appears Given I am a logged in customer And I added product "PHP T-Shirt" to the cart - And I have proceeded selecting "Offline" payment method + And I proceeded through checkout process And this product price has been changed to "$25.00" And I have confirmed order - When I confirm my order - Then I should see the thank you page + Then my order should not be placed due to changed order total - @ui + @ui @api Scenario: Inform customer about order total change due to tax change Given I am a logged in customer And I added product "PHP T-Shirt" to the cart - And I have proceeded selecting "Offline" payment method + And I proceeded through checkout process And the "NA VAT" tax rate has changed to 10% When I confirm my order - Then I should be informed that order total has been changed - And I should not see the thank you page + Then my order should not be placed due to changed order total - @ui + @ui @api Scenario: Inform customer about order total change due to shipping method fee change Given the store has "UPS" shipping method with "$20.00" fee And I added product "PHP T-Shirt" to the cart @@ -50,5 +47,4 @@ Feature: Inform customer about any order total changes during checkout process And I have proceeded order with "UPS" shipping method and "Offline" payment And the shipping fee for "UPS" shipping method has been changed to "$30.00" When I confirm my order - Then I should be informed that order total has been changed - And I should not see the thank you page + Then my order should not be placed due to changed order total diff --git a/features/checkout/paying_for_order/paying_offline_during_checkout.feature b/features/checkout/paying_for_order/paying_offline_during_checkout.feature index 10af63302e..656e73eb2c 100644 --- a/features/checkout/paying_for_order/paying_offline_during_checkout.feature +++ b/features/checkout/paying_for_order/paying_offline_during_checkout.feature @@ -10,10 +10,11 @@ Feature: Paying offline during checkout And the store ships everywhere for Free And the store allows paying Offline - @ui + @ui @api Scenario: Successfully placing an order Given I am a logged in customer And I have product "PHP T-Shirt" in the cart - When I proceed selecting "Offline" payment method + And I have specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" + When I proceeded with "Free" shipping method and "Offline" payment method And I confirm my order Then I should see the thank you page diff --git a/features/checkout/paying_for_order/paying_offline_during_checkout_as_guest.feature b/features/checkout/paying_for_order/paying_offline_during_checkout_as_guest.feature index 0549d58c46..1c2b35f6f1 100644 --- a/features/checkout/paying_for_order/paying_offline_during_checkout_as_guest.feature +++ b/features/checkout/paying_for_order/paying_offline_during_checkout_as_guest.feature @@ -20,9 +20,17 @@ Feature: Paying Offline during checkout as guest And I confirm my order Then I should see the thank you page - @ui + @ui @no-api Scenario: Successfully placing an order using custom locale Given I have product "PHP T-Shirt" in the cart When I proceed through checkout process in the "French (France)" locale with email "john@example.com" And I confirm my order Then I should see the thank you page in "French (France)" + + @api @no-ui + Scenario: Successfully placing an order using custom locale + Given I pick up cart in the "French (France)" locale + And I add product "PHP T-Shirt" to the cart + When I proceed through checkout process + And I confirm my order + Then my order's locale should be "French (France)" diff --git a/features/checkout/paying_for_order/preventing_changing_the_payment_method_of_a_cancelled_order.feature b/features/checkout/paying_for_order/preventing_changing_the_payment_method_of_a_cancelled_order.feature new file mode 100644 index 0000000000..3f90777675 --- /dev/null +++ b/features/checkout/paying_for_order/preventing_changing_the_payment_method_of_a_cancelled_order.feature @@ -0,0 +1,24 @@ +@paying_for_order +Feature: Preventing changing the payment method of a cancelled order + In order to perform only valid order operations + As a Customer + I should be prevented from changing the payment method of a cancelled order + + Background: + Given the store operates on a single channel in "United States" + And the store has a product "Angel T-Shirt" + And the store ships everywhere for Free + And the store allows paying with "Cash on Delivery" + And the store also allows paying with "Bank Transfer" + And I am a logged in customer + And I placed an order "#00000666" + And I bought a single "Angel T-Shirt" + And I addressed it to "Lucifer Morningstar", "Seaside Fwy", "90802" "Los Angeles" in the "United States" with identical billing address + And I chose "Free" shipping method with "Cash on Delivery" payment + And I am changing this order's payment method + And this order was cancelled + + @ui @api + Scenario: Being prevented from changing the payment method of a cancelled order + When I try to change my payment method to "Bank Transfer" + Then I should be notified that I can no longer change payment method of this order diff --git a/features/checkout/paying_for_order/preventing_to_pay_for_cancelled_order.feature b/features/checkout/paying_for_order/preventing_to_pay_for_cancelled_order.feature index d0ba942a96..ff3246c153 100644 --- a/features/checkout/paying_for_order/preventing_to_pay_for_cancelled_order.feature +++ b/features/checkout/paying_for_order/preventing_to_pay_for_cancelled_order.feature @@ -13,7 +13,7 @@ Feature: Preventing to pay for the cancelled order And the store allows paying Offline And there is a customer "sylius@example.com" that placed an order "#00000022" - @ui + @ui @no-api Scenario: Not being able to pay for cancelled order Given the customer bought 3 "Iron Maiden T-Shirt" products And the customer chose "Free" shipping method to "United States" with "Offline" payment diff --git a/features/checkout/paying_for_order/seeing_payment_method_instructions_on_thank_you_page.feature b/features/checkout/paying_for_order/seeing_payment_method_instructions_after_checkout.feature similarity index 72% rename from features/checkout/paying_for_order/seeing_payment_method_instructions_on_thank_you_page.feature rename to features/checkout/paying_for_order/seeing_payment_method_instructions_after_checkout.feature index e04dab4860..57b01ac015 100644 --- a/features/checkout/paying_for_order/seeing_payment_method_instructions_on_thank_you_page.feature +++ b/features/checkout/paying_for_order/seeing_payment_method_instructions_after_checkout.feature @@ -1,5 +1,5 @@ @paying_for_order -Feature: Seeing payment method instructions on thank you page +Feature: Seeing payment method instructions after checkout In order to know how to pay for my order As a Customer I want to be informed about payment instructions for chosen payment method @@ -11,11 +11,10 @@ Feature: Seeing payment method instructions on thank you page And the store has a payment method "Offline" with a code "OFFLINE" And it has instructions "Account number: 0000 1111 2222 3333" - @ui - Scenario: Being informed about payment instructions on thank you page + @ui @api + Scenario: Being informed about payment instructions Given I am a logged in customer And I have product "PHP T-Shirt" in the cart When I proceed selecting "Offline" payment method And I confirm my order - Then I should see the thank you page - And I should be informed with "Offline" payment method instructions + Then I should be informed with "Offline" payment method instructions diff --git a/features/checkout/placing_an_order_with_different_scopes_for_shipping_and_taxes.feature b/features/checkout/placing_an_order_with_different_scopes_for_shipping_and_taxes.feature index 7621949e11..fc185f4fff 100644 --- a/features/checkout/placing_an_order_with_different_scopes_for_shipping_and_taxes.feature +++ b/features/checkout/placing_an_order_with_different_scopes_for_shipping_and_taxes.feature @@ -8,7 +8,7 @@ Feature: Placing an order with different scopes for shipping and taxes Given the store operates on a single channel in "USD" currency And the store operates in "United States" And the store operates in "Germany" - And the store has a product "Jane's Vest" priced at "$20" + And the store has a product "Jane's Vest" priced at "$20.00" And the store allows paying Offline And I am a logged in customer @@ -58,4 +58,4 @@ Feature: Placing an order with different scopes for shipping and taxes And I complete the addressing step And I proceed with "Free" shipping method and "Offline" payment Then I should be on the checkout summary step - And my order total should be "$20" + And my order total should be "$20.00" diff --git a/features/checkout/product_integrity_validation.feature b/features/checkout/product_integrity_validation.feature index f6e41afe27..214dff8a15 100644 --- a/features/checkout/product_integrity_validation.feature +++ b/features/checkout/product_integrity_validation.feature @@ -23,7 +23,7 @@ Feature: Order products integrity Then I should be informed that this product has been disabled And I should not see the thank you page - @ui + @ui @api Scenario: Preventing customer from completing checkout with no longer available product variant Given I have "Small" variant of product "Super Cool T-Shirt" in the cart And I have proceeded selecting "Offline" payment method diff --git a/features/checkout/registering_account_after_checkout.feature b/features/checkout/registering_account_after_checkout.feature index 5e2e64c275..cafaee1798 100644 --- a/features/checkout/registering_account_after_checkout.feature +++ b/features/checkout/registering_account_after_checkout.feature @@ -11,8 +11,22 @@ Feature: Registering a new account after checkout And the store allows paying Offline @ui @no-api - Scenario: Registering a new account after checkout - Given I have product "PHP T-Shirt" in the cart + Scenario: Displaying thank you page after registration + Given on this channel account verification is required + And I have product "PHP T-Shirt" in the cart + And I have completed addressing step with email "john@example.com" and "United States" based billing address + And I have proceeded order with "Free" shipping method and "Offline" payment + And I have confirmed order + When I proceed to the registration + And I specify a password as "sylius" + And I confirm this password + And I register this account + Then I should be on registration thank you page + + @ui @no-api + Scenario: Registering a new account after checkout when channel has enabled registration verification + Given on this channel account verification is required + And I have product "PHP T-Shirt" in the cart And I have completed addressing step with email "john@example.com" and "United States" based billing address And I have proceeded order with "Free" shipping method and "Offline" payment And I have confirmed order @@ -22,3 +36,16 @@ Feature: Registering a new account after checkout And I register this account And I verify my account using link sent to "john@example.com" Then I should be able to log in as "john@example.com" with "sylius" password + + @ui @no-api + Scenario: Registering a new account after checkout when channel has disabled registration verification + Given on this channel account verification is not required + And I have product "PHP T-Shirt" in the cart + And I have completed addressing step with email "john@example.com" and "United States" based billing address + And I have proceeded order with "Free" shipping method and "Offline" payment + And I have confirmed order + When I proceed to the registration + And I specify a password as "sylius" + And I confirm this password + And I register this account + Then I should be on my account dashboard diff --git a/features/checkout/seeing_order_summary/seeing_discount_on_order_item_on_summary_page.feature b/features/checkout/seeing_order_summary/seeing_discount_on_order_item_on_summary_page.feature index b416a590b5..fec4aa9ce0 100644 --- a/features/checkout/seeing_order_summary/seeing_discount_on_order_item_on_summary_page.feature +++ b/features/checkout/seeing_order_summary/seeing_discount_on_order_item_on_summary_page.feature @@ -19,5 +19,5 @@ Feature: Seeing a order item discount And I specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" When I proceed with "Free" shipping method and "Offline" payment Then I should be on the checkout summary step - And the "Lannister Coat" product should have unit price discounted by "$10" - And my order total should be "$90" + And the "Lannister Coat" product should have unit price discounted by "$10.00" + And my order total should be "$90.00" diff --git a/features/checkout/seeing_order_summary/seeing_order_locale.feature b/features/checkout/seeing_order_summary/seeing_order_locale.feature index 91046d4dc5..e207d89a64 100644 --- a/features/checkout/seeing_order_summary/seeing_order_locale.feature +++ b/features/checkout/seeing_order_summary/seeing_order_locale.feature @@ -19,9 +19,17 @@ Feature: Seeing order locale on order summary page Then I should be on the checkout summary step And my order's locale should be "English (United States)" - @ui + @ui @no-api Scenario: Seeing order locale on the order summary page after change channel locale Given I have product "Stark T-Shirt" in the cart When I proceed through checkout process in the "French (France)" locale Then I should be on the checkout summary step And my order's locale should be "French (France)" + + @api @no-ui + Scenario: Seeing order locale on the order summary page after change channel locale + Given I pick up cart in the "French (France)" locale + And I have product "Stark T-Shirt" in the cart + When I proceed through checkout process + Then I should be on the checkout summary step + And my order's locale should be "French (France)" diff --git a/features/checkout/seeing_purchaser_identifier_in_checkout_page.feature b/features/checkout/seeing_purchaser_identifier_in_checkout_page.feature index b0eb1ba9e7..8960db42f6 100644 --- a/features/checkout/seeing_purchaser_identifier_in_checkout_page.feature +++ b/features/checkout/seeing_purchaser_identifier_in_checkout_page.feature @@ -6,7 +6,7 @@ Feature: Seeing purchaser identifier in checkout page Background: Given the store operates on a single channel in "United States" - And the store has a product "Gaming chair" priced at "$399" + And the store has a product "Gaming chair" priced at "$399.00" And the store ships everywhere for Free And the store allows paying Offline And there is a customer "John Doe" identified by an email "john@example.com" and a password "secret" diff --git a/features/checkout/shipping_order/preventing_not_available_shipping_method_selection.feature b/features/checkout/shipping_order/preventing_not_available_shipping_method_selection.feature index 39a272723d..f1b7fb778d 100644 --- a/features/checkout/shipping_order/preventing_not_available_shipping_method_selection.feature +++ b/features/checkout/shipping_order/preventing_not_available_shipping_method_selection.feature @@ -80,7 +80,7 @@ Feature: Preventing not available shipping method selection Scenario: Not being able to select shipping method not available due to shipping rules Given the store has "Raven Post" shipping method with "$10.00" fee And the store has "Dragon Post" shipping method with "$30.00" fee - And this shipping method is only available for orders over or equal to "$100" + And this shipping method is only available for orders over or equal to "$100.00" And I have product "Targaryen T-Shirt" in the cart And I am at the checkout addressing step When I specify the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" diff --git a/features/checkout/shipping_order/seeing_default_shipping_method_selected_based_on_shipping_address.feature b/features/checkout/shipping_order/seeing_default_shipping_method_selected_based_on_shipping_address.feature index 3970c9dc66..1f0341808c 100644 --- a/features/checkout/shipping_order/seeing_default_shipping_method_selected_based_on_shipping_address.feature +++ b/features/checkout/shipping_order/seeing_default_shipping_method_selected_based_on_shipping_address.feature @@ -17,7 +17,7 @@ Feature: Seeing default shipping method selected based on shipping address And the store has "FedEx" shipping method with "$20.00" fee within the "UK" zone And I am a logged in customer - @ui + @ui @api Scenario: Seeing default shipping method selected based on country from billing address Given I have product "Star Trek Ship" in the cart And I am at the checkout addressing step @@ -27,7 +27,7 @@ Feature: Seeing default shipping method selected based on shipping address And I should see selected "DHL" shipping method And I should not see "FedEx" shipping method - @ui + @ui @api Scenario: Seeing default shipping method selected based on country from billing address after readdressing Given I have product "Star Trek Ship" in the cart And I am at the checkout addressing step diff --git a/features/cli/change_admin_password.feature b/features/cli/change_admin_password.feature new file mode 100644 index 0000000000..15cd718cb4 --- /dev/null +++ b/features/cli/change_admin_password.feature @@ -0,0 +1,17 @@ +@change_admin_password +Feature: Changing an administrator's password via CLI + In order to login to my administrator account when I forget my password + As a administrator + I want to be able to change my administrator password via CLI + + Background: + Given there is an administrator "sylius_change_password@example.com" identified by "sylius" + + @cli + Scenario: Changing an administrator's password + When I want to change password + And I specify email as "sylius_change_password@example.com" + And I specify my new password as "newp@ssw0rd" + And I run command + Then I should be informed that password has been changed successfully + And I should be able to log in as "sylius_change_password@example.com" authenticated by "newp@ssw0rd" password diff --git a/features/currency/handling_different_currencies_on_multiple_channels.feature b/features/currency/handling_different_currencies_on_multiple_channels.feature index 8602f1a681..c14c2d9b45 100644 --- a/features/currency/handling_different_currencies_on_multiple_channels.feature +++ b/features/currency/handling_different_currencies_on_multiple_channels.feature @@ -5,19 +5,19 @@ Feature: Handling different currencies on multiple channels I want to browse channels with a valid currency only Background: - Given the store operates on a channel named "Web" in "EUR" currency + Given the store operates on a channel named "Web" in "EUR" currency and with hostname "web.localhost.example" And that channel allows to shop using "EUR", "USD" and "GBP" currencies - And the store operates on another channel named "Mobile" in "USD" currency + And the store operates on another channel named "Mobile" in "USD" currency and with hostname "m.localhost.example" And that channel allows to shop using "USD" and "GBP" currencies - @ui + @ui @api Scenario: Showing currencies only from the current channel When I browse the "Mobile" channel Then I should shop using the "USD" currency And I should be able to shop using the "GBP" currency And I should not be able to shop using the "EUR" currency - @ui + @ui @api Scenario: Browsing channels using their default currencies When I browse the "Web" channel And I start browsing the "Mobile" channel diff --git a/features/currency/managing_currencies/editing_currency.feature b/features/currency/managing_currencies/editing_currency.feature index 7e8a11bb65..060be02b8a 100644 --- a/features/currency/managing_currencies/editing_currency.feature +++ b/features/currency/managing_currencies/editing_currency.feature @@ -7,7 +7,7 @@ Feature: Editing a currency Background: Given I am logged in as an administrator - @ui + @ui @no-api Scenario: Seeing disabled code field while editing currency Given the store has currency "Euro" When I want to edit this currency diff --git a/features/currency/managing_exchange_rates/deleting_multiple_exchange_rates.feature b/features/currency/managing_exchange_rates/deleting_multiple_exchange_rates.feature index 4ee3833c3e..2594303eb9 100644 --- a/features/currency/managing_exchange_rates/deleting_multiple_exchange_rates.feature +++ b/features/currency/managing_exchange_rates/deleting_multiple_exchange_rates.feature @@ -11,7 +11,7 @@ Feature: Deleting multiple exchange rates And the exchange rate of "Polish Zloty" to "Euro" is 0.22 And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple exchange rates at once When I browse exchange rates And I check the exchange rate between "Euro" and "British Pound" diff --git a/features/homepage/viewing_only_enabled_taxons_in_taxon_menu.feature b/features/homepage/viewing_only_enabled_taxons_in_taxon_menu.feature index dbabf25fee..96a4050d3c 100644 --- a/features/homepage/viewing_only_enabled_taxons_in_taxon_menu.feature +++ b/features/homepage/viewing_only_enabled_taxons_in_taxon_menu.feature @@ -12,15 +12,7 @@ Feature: Viewing only enabled taxons in taxon menu And the "Accessories" taxon has children taxons "Caps" and "Belts" And channel "United States" has menu taxon "Category" - @ui - Scenario: Viewing only enabled taxons in taxon menu - Given the "Clothes" taxon is disabled - And the "Belts" taxon is disabled - When I check available taxons - Then I should see "Accessories Caps" and "Caps" in the menu - And I should not see "Clothes", "T-Shirts", "Coats" and "Belts" in the menu - - @api + @api @ui Scenario: Viewing only enabled taxons in taxon menu Given the "Clothes" taxon is disabled And the "Belts" taxon is disabled diff --git a/features/inventory/cart_inventory/verify_inventory_quantity_when_updating_cart_summary.feature b/features/inventory/cart_inventory/verify_inventory_quantity_when_updating_cart_summary.feature index 9cff3118f2..3b174f4f06 100644 --- a/features/inventory/cart_inventory/verify_inventory_quantity_when_updating_cart_summary.feature +++ b/features/inventory/cart_inventory/verify_inventory_quantity_when_updating_cart_summary.feature @@ -15,15 +15,15 @@ Feature: Verifying inventory quantity on cart summary @ui @api Scenario: Being unable to save a cart with product that is out of stock - Given I have added 3 products "Iron Maiden T-Shirt" in the cart + Given I have added 3 products "Iron Maiden T-Shirt" to the cart When I change product "Iron Maiden T-Shirt" quantity to 6 in my cart And I update my cart Then I should be notified that this product has insufficient stock @ui @no-api Scenario: Preventing the cart recalculation when the form has errors - Given I have added 3 products "Iron Maiden T-Shirt" in the cart - And I have added 1 products "Black Dress" in the cart + Given I have added 3 products "Iron Maiden T-Shirt" to the cart + And I have added 1 product "Black Dress" to the cart When I change product "Iron Maiden T-Shirt" quantity to 4 in my cart And I change product "Black Dress" quantity to 11 in my cart Then I should be notified that this product has insufficient stock diff --git a/features/inventory/checkout_inventory/holding_inventory_units_during_checkout_process.feature b/features/inventory/checkout_inventory/holding_inventory_units_during_checkout_process.feature index 62e4b8dff0..d1cfd0a1cb 100644 --- a/features/inventory/checkout_inventory/holding_inventory_units_during_checkout_process.feature +++ b/features/inventory/checkout_inventory/holding_inventory_units_during_checkout_process.feature @@ -14,7 +14,7 @@ Feature: Holding inventory units during checkout And there is a customer "sylius@example.com" that placed an order "#00000022" And I am logged in as an administrator - @ui + @ui @api Scenario: Holding inventory units Given the customer bought 3 "Iron Maiden T-Shirt" products And the customer chose "Free" shipping method to "United States" with "Offline" payment @@ -22,7 +22,7 @@ Feature: Holding inventory units during checkout Then 3 units of this product should be on hold And 5 units of this product should be on hand - @ui + @ui @api Scenario: Release hold units after order has been paid Given the customer bought 3 "Iron Maiden T-Shirt" products And the customer chose "Free" shipping method to "United States" with "Offline" payment diff --git a/features/inventory/managing_inventory/browsing_inventory.feature b/features/inventory/managing_inventory/browsing_inventory.feature index 6daa68e2e4..db1e3668fa 100644 --- a/features/inventory/managing_inventory/browsing_inventory.feature +++ b/features/inventory/managing_inventory/browsing_inventory.feature @@ -13,12 +13,12 @@ Feature: Browsing inventory And there are 5 units of product "Iron Maiden T-Shirt" available in the inventory And I am logged in as an administrator - @ui + @ui @no-api Scenario: Browsing only tracked product variants in the store When I want to browse inventory Then I should see only one tracked variant in the list - @ui + @ui @no-api Scenario: Being informed about on hand quantity of a product variant When I want to browse inventory Then I should see that the "Iron Maiden T-Shirt" variant has 5 quantity on hand diff --git a/features/inventory/managing_inventory/decreasing_invetory_below_on_hold_validation.feature b/features/inventory/managing_inventory/decreasing_invetory_below_on_hold_validation.feature index c60b39da36..bfd87d11b5 100644 --- a/features/inventory/managing_inventory/decreasing_invetory_below_on_hold_validation.feature +++ b/features/inventory/managing_inventory/decreasing_invetory_below_on_hold_validation.feature @@ -17,7 +17,7 @@ Feature: Validation of decreasing inventory below on hold validation And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @ui @api Scenario: Decreasing inventory when order was placed When I want to modify the "Wyborowa Vodka Exquisite" product variant And I change its quantity of inventory to 2 @@ -25,7 +25,7 @@ Feature: Validation of decreasing inventory below on hold validation Then I should be notified that on hand quantity must be greater than the number of on hold units And this variant should have a 5 item currently in stock - @ui + @ui @api Scenario: Decreasing inventory when order was cancelled Given the order "#00000023" was cancelled When I want to modify the "Wyborowa Vodka Exquisite" product variant @@ -34,7 +34,7 @@ Feature: Validation of decreasing inventory below on hold validation Then I should be notified that it has been successfully edited And this variant should have a 2 item currently in stock - @ui + @ui @api Scenario: Decreasing inventory when order was paid Given the order "#00000023" is already paid When I want to modify the "Wyborowa Vodka Exquisite" product variant diff --git a/features/inventory/managing_inventory/filtering_inventory_by_code.feature b/features/inventory/managing_inventory/filtering_inventory_by_code.feature index 6952b9b1d5..a2e2445fe5 100644 --- a/features/inventory/managing_inventory/filtering_inventory_by_code.feature +++ b/features/inventory/managing_inventory/filtering_inventory_by_code.feature @@ -14,7 +14,7 @@ Feature: Filtering inventory by code And there are 25 units of product "RHCP T-Shirt" available in the inventory And I am logged in as an administrator - @ui + @ui @no-api Scenario: Filtering tracked product variants by code When I want to browse inventory And I filter tracked variants with code containing "iron" diff --git a/features/inventory/managing_inventory/filtering_inventory_by_name.feature b/features/inventory/managing_inventory/filtering_inventory_by_name.feature index efa370593b..619ed89062 100644 --- a/features/inventory/managing_inventory/filtering_inventory_by_name.feature +++ b/features/inventory/managing_inventory/filtering_inventory_by_name.feature @@ -14,7 +14,7 @@ Feature: Filtering inventory by name And there are 25 units of product "RHCP T-Shirt" available in the inventory And I am logged in as an administrator - @ui + @ui @no-api Scenario: Filtering tracked product variants by name When I want to browse inventory And I filter tracked variants with name containing "RHCP" diff --git a/features/inventory/verify_reserved_inventory_is_back_in_stock_on_order_cancelling.feature b/features/inventory/managing_inventory/verify_reserved_inventory_is_back_in_stock_on_order_cancelling.feature similarity index 98% rename from features/inventory/verify_reserved_inventory_is_back_in_stock_on_order_cancelling.feature rename to features/inventory/managing_inventory/verify_reserved_inventory_is_back_in_stock_on_order_cancelling.feature index 985ee6135d..c99f2d795f 100644 --- a/features/inventory/verify_reserved_inventory_is_back_in_stock_on_order_cancelling.feature +++ b/features/inventory/managing_inventory/verify_reserved_inventory_is_back_in_stock_on_order_cancelling.feature @@ -18,7 +18,7 @@ Feature: Inventory releasing on order cancellation And the store allows paying with "Cash on Delivery" And I am logged in as an administrator - @ui + @ui @api Scenario: Verify the reserved inventory is back in stock after cancellation of a new order Given there is a customer "john.doe@gmail.com" that placed an order "#00000022" And the customer bought 3 units of "Green" variant of product "T-Shirt banana" @@ -28,7 +28,7 @@ Feature: Inventory releasing on order cancellation Then the variant "Green" should have 5 items on hand And the "Green" variant should have 0 items on hold - @ui + @ui @api Scenario: Verify the reserved inventory and quantity of product's items is back in stock after cancellation of paid order Given there is a customer "john.doe@gmail.com" that placed an order "#00000022" And the customer bought 3 units of "Green" variant of product "T-Shirt banana" @@ -39,7 +39,7 @@ Feature: Inventory releasing on order cancellation Then the variant "Green" should have 5 items on hand And the "Green" variant should have 0 items on hold - @ui + @ui @api Scenario: Verify the reserved inventory is back in stock after cancellation of a new order with two variants of product Given there is a customer "john.doe@gmail.com" that placed an order "#00000023" And the customer bought 3 units of "Green" variant of product "T-Shirt banana" @@ -52,7 +52,7 @@ Feature: Inventory releasing on order cancellation And the variant "Red" should have 5 items on hand And the "Red" variant should have 0 items on hold - @ui + @ui @api Scenario: Verify the reserved inventory and quantity of product's items is back in stock after cancellation of paid order with two variants of product Given there is a customer "john.doe@gmail.com" that placed an order "#00000023" And the customer bought 3 units of "Green" variant of product "T-Shirt banana" @@ -66,7 +66,7 @@ Feature: Inventory releasing on order cancellation And the variant "Red" should have 5 items on hand And the "Red" variant should have 0 items on hold - @ui + @ui @api Scenario: Verify the reserved inventory is back in stock after cancellation of a new order with two variants of different products Given there is a customer "john.doe@gmail.com" that placed an order "#00000024" And the customer bought 3 units of "Green" variant of product "T-Shirt banana" @@ -78,7 +78,7 @@ Feature: Inventory releasing on order cancellation And the "Yellow" variant of "Skirt watermelon" product should have 5 items on hand And the "Yellow" variant of "Skirt watermelon" product should have 0 items on hold - @ui + @ui @api Scenario: Verify the reserved inventory and quantity of product's items is back in stock after cancellation of paid order with two variants of different products Given there is a customer "john.doe@gmail.com" that placed an order "#00000024" And the customer bought 3 units of "Green" variant of product "T-Shirt banana" @@ -91,7 +91,7 @@ Feature: Inventory releasing on order cancellation And the "Yellow" variant of "Skirt watermelon" product should have 5 items on hand And the "Yellow" variant of "Skirt watermelon" product should have 0 items on hold - @ui + @ui @api Scenario: Verify the reserved inventory and quantity of product's items is back in stock after cancellation of a refunded order Given there is a customer "john.doe@gmail.com" that placed an order "#00000022" And the customer bought 3 units of "Green" variant of product "T-Shirt banana" diff --git a/features/inventory/viewing_product_variant_with_specify_quantity_of_items_on_hand.feature b/features/inventory/managing_inventory/viewing_product_variant_with_specify_quantity_of_items_on_hand.feature similarity index 98% rename from features/inventory/viewing_product_variant_with_specify_quantity_of_items_on_hand.feature rename to features/inventory/managing_inventory/viewing_product_variant_with_specify_quantity_of_items_on_hand.feature index 221d4c1c49..a9313f2682 100644 --- a/features/inventory/viewing_product_variant_with_specify_quantity_of_items_on_hand.feature +++ b/features/inventory/managing_inventory/viewing_product_variant_with_specify_quantity_of_items_on_hand.feature @@ -15,7 +15,7 @@ Feature: Seeing product's variant with specify quantity of items on hand And the store allows paying with "Cash on Delivery" And I am logged in as an administrator - @ui + @ui @api Scenario: Seeing decreased quantity of product's items in selected variant after order payment Given there is a customer "lucy@teamlucifer.com" that placed an order "#00000666" And the customer bought 3 units of "Green" variant of product "T-Shirt banana" @@ -25,7 +25,7 @@ Feature: Seeing product's variant with specify quantity of items on hand When I view all variants of the product "T-Shirt banana" Then the variant "Green" should have 2 items on hand - @ui + @ui @api Scenario: Seeing decreased quantity of product's items from different variants after order payment Given there is a customer "lucy@teamlucifer.com" that placed an order "#00000666" And the customer bought 3 units of "Yellow" variant of product "T-Shirt banana" diff --git a/features/locale/managing_locales/removing_locale.feature b/features/locale/managing_locales/removing_locale.feature new file mode 100644 index 0000000000..233251437e --- /dev/null +++ b/features/locale/managing_locales/removing_locale.feature @@ -0,0 +1,29 @@ +@managing_locales +Feature: Removing locales + In order to delete accidentally created locales + As an Administrator + I want to be able to delete locales + + Background: + Given the store operates on a channel named "Web" with hostname "web" + And that channel allows to shop using "English (United States)" and "Polish (Poland)" locales + And it uses the "English (United States)" locale by default + And I am logged in as an administrator + + @ui @api + Scenario: Deleting unused locale + Given the store has a product "T-Shirt banana" + And this product is named "Banana T-Shirt with Minions" in the "English (United States)" locale + And this product has no translation in the "Polish (Poland)" locale + When I remove "Polish (Poland)" locale + Then I should be informed that locale "Polish (Poland)" has been deleted + And only the "English (United States)" locale should be present in the system + + @ui @api + Scenario: Deleting a locale in use + Given the store has a product "T-Shirt banana" + And this product is named "Banana T-Shirt with Minions" in the "English (United States)" locale + And this product is named "Koszulka Banan z Minionami" in the "Polish (Poland)" locale + When I remove "Polish (Poland)" locale + Then I should be informed that locale "Polish (Poland)" is in use and cannot be deleted + And the "Polish (Poland)" locale should be still present in the system diff --git a/features/order/managing_orders/browsing_orders/seeing_orders_currency_on_index.feature b/features/order/managing_orders/browsing_orders/seeing_orders_currency_on_index.feature index aaff5f6e58..0fcc81f916 100644 --- a/features/order/managing_orders/browsing_orders/seeing_orders_currency_on_index.feature +++ b/features/order/managing_orders/browsing_orders/seeing_orders_currency_on_index.feature @@ -17,7 +17,7 @@ Feature: Seeing the currency in which all orders have been placed And there is a customer "Satin" identified by an email "satin@teamlucifer.com" and a password "pswd" And I am logged in as an administrator - @ui + @ui @api Scenario: Seeing an order placed in the base currency Given there is a customer "lucy@teamlucifer.com" that placed an order "#00000666" And the customer bought a single "Angel T-Shirt" diff --git a/features/order/managing_orders/browsing_orders/seeing_orders_with_different_currency_on_index.feature b/features/order/managing_orders/browsing_orders/seeing_orders_with_different_currency_on_index.feature index 22cbc0e4ca..3d7dbd8726 100644 --- a/features/order/managing_orders/browsing_orders/seeing_orders_with_different_currency_on_index.feature +++ b/features/order/managing_orders/browsing_orders/seeing_orders_with_different_currency_on_index.feature @@ -17,7 +17,7 @@ Feature: Seeing orders' total in their currency And there is a customer account "customer@example.com" identified by "sylius" And I am logged in as "customer@example.com" - @ui + @ui @api Scenario: List of orders from only one channel Given I changed my current channel to "United States" And I have product "Angel T-Shirt" in the cart @@ -26,7 +26,7 @@ Feature: Seeing orders' total in their currency And I confirm my order Then the administrator should see the order with total "$20.00" in order list - @ui + @ui @api Scenario: List of orders from different channels Given I changed my current channel to "United States" And I have product "Angel T-Shirt" in the cart diff --git a/features/order/managing_orders/browsing_orders/sequential_order_number_generation.feature b/features/order/managing_orders/browsing_orders/sequential_order_number_generation.feature index b1a77f27df..9618ec5bc7 100644 --- a/features/order/managing_orders/browsing_orders/sequential_order_number_generation.feature +++ b/features/order/managing_orders/browsing_orders/sequential_order_number_generation.feature @@ -17,8 +17,8 @@ Feature: Sequential order number generation And the customer chose "Free" shipping method to "United States" with "Offline" payment And I am logged in as an administrator - @ui + @ui @api Scenario: Verifying that orders has correct numbers When I browse orders - Then I should see an order with "#00000001" number - And I should see an order with "#00000002" number + Then I should see an order with "#000000001" number + And I should see an order with "#000000002" number diff --git a/features/order/managing_orders/browsing_orders/sorting_orders_by_number.feature b/features/order/managing_orders/browsing_orders/sorting_orders_by_number.feature index f9d14b151b..4af17fe610 100644 --- a/features/order/managing_orders/browsing_orders/sorting_orders_by_number.feature +++ b/features/order/managing_orders/browsing_orders/sorting_orders_by_number.feature @@ -20,13 +20,13 @@ Feature: Sorting orders by their number And the customer chose "Free" shipping method to "United States" with "Offline" payment And I am logged in as an administrator - @ui + @ui @api Scenario: Orders are sorted by descending numbers by default When I browse orders Then I should see an order with "#000000001" number But the first order should have number "#000000003" - @ui + @ui @api Scenario: Changing the number sorting order Given I am browsing orders When I switch the way orders are sorted by number diff --git a/features/order/managing_orders/cancelling_order_with_promotion.feature b/features/order/managing_orders/cancelling_order_with_promotion.feature index e7074ce617..968d08ffeb 100644 --- a/features/order/managing_orders/cancelling_order_with_promotion.feature +++ b/features/order/managing_orders/cancelling_order_with_promotion.feature @@ -10,7 +10,7 @@ Feature: Cancelling order with promotion applied And the store allows paying with "Cash on Delivery" And the store has a product "Suit" priced at "$400.00" And there is a promotion "Holiday promotion" - And the promotion gives "$50" discount to every order + And the promotion gives "$50.00" discount to every order And there is a customer "mike@ross.com" that placed an order "#00000001" And the customer bought a single "Suit" And the customer "Mike Ross" addressed it to "350 5th Ave", "10118" "New York" in the "United States" with identical billing address diff --git a/features/order/managing_orders/handling_order_shipment/placing_order_with_shipment.feature b/features/order/managing_orders/handling_order_shipment/placing_order_with_shipment.feature index 29fd52e4e9..bcb828f2eb 100644 --- a/features/order/managing_orders/handling_order_shipment/placing_order_with_shipment.feature +++ b/features/order/managing_orders/handling_order_shipment/placing_order_with_shipment.feature @@ -15,7 +15,7 @@ Feature: Shipments are in the state "ready" after checkout And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @ui @api Scenario: Checking shipment state of a placed order When I view the summary of the order "#00000666" Then it should have shipment in state "Ready" diff --git a/features/order/managing_orders/handling_order_shipment/resending_shipment_confirmation_email.feature b/features/order/managing_orders/handling_order_shipment/resending_shipment_confirmation_email.feature index ec67a01c07..e46c31191b 100644 --- a/features/order/managing_orders/handling_order_shipment/resending_shipment_confirmation_email.feature +++ b/features/order/managing_orders/handling_order_shipment/resending_shipment_confirmation_email.feature @@ -17,17 +17,26 @@ Feature: Resending a shipment confirmation email for a chosen order And this order has already been shipped And I am logged in as an administrator - @ui @email + @ui @email @api Scenario: Resending a shipment confirmation email for a given order When I view the summary of the order "#00000666" And I resend the shipment confirmation email Then I should be notified that the shipment confirmation email has been successfully resent to the customer And an email with the shipment's confirmation of the order "#00000666" should be sent to "lucy@teamlucifer.com" - @ui @email + @ui @email @api Scenario: Resending a shipment confirmation email after shipping an order in different locale than the default one Given the order "#00000666" has been placed in "Polish (Poland)" locale When I view the summary of the order "#00000666" And I resend the shipment confirmation email Then I should be notified that the shipment confirmation email has been successfully resent to the customer And an email with the shipment's confirmation of the order "#00000666" should be sent to "lucy@teamlucifer.com" in "Polish (Poland)" locale + + @ui @email @api + Scenario: Not being able to resend a confirmation email for a given shipment with wrong state + Given this customer placed another order "#00000023" + And the customer bought a single "Angel T-Shirt" + And the customer "Lucifer Morningstar" addressed it to "Seaside Fwy", "90802" "Los Angeles" in the "United States" with identical billing address + And the customer chose "Free" shipping method with "Cash on Delivery" payment + When I view the summary of the order "#00000023" + Then I should not be able to resend the shipment confirmation email diff --git a/features/order/managing_orders/handling_order_shipment/shipping_order.feature b/features/order/managing_orders/handling_order_shipment/shipping_order.feature index e628d09344..a697dbe5e6 100644 --- a/features/order/managing_orders/handling_order_shipment/shipping_order.feature +++ b/features/order/managing_orders/handling_order_shipment/shipping_order.feature @@ -15,7 +15,7 @@ Feature: Shipping an order And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @ui @api Scenario: Finalizing order's shipment Given I view the summary of the order "#00000666" When specify its tracking code as "#00044" @@ -23,13 +23,13 @@ Feature: Shipping an order Then I should be notified that the order has been successfully shipped And it should have shipment in state shipped - @ui + @ui @api Scenario: Unable to finalize shipped order's shipment Given this order has already been shipped When I view the summary of the order "#00000666" Then I should not be able to ship this order - @ui + @ui @api Scenario: Checking the shipment state of a completed order Given this order has already been shipped When I browse orders diff --git a/features/order/managing_orders/modifying_billing_address/modifying_billing_address.feature b/features/order/managing_orders/modifying_billing_address/modifying_billing_address.feature index 1bfb5d1817..b169b37783 100644 --- a/features/order/managing_orders/modifying_billing_address/modifying_billing_address.feature +++ b/features/order/managing_orders/modifying_billing_address/modifying_billing_address.feature @@ -1,4 +1,4 @@ -@modifying_address +@modifying_placed_order_address Feature: Modifying a customer billing address after an order has been placed In order to ship an order's bill to a correct place As an Administrator @@ -15,16 +15,16 @@ Feature: Modifying a customer billing address after an order has been placed And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Modifying a customer's billing address When I view the summary of the order "#00000001" And I want to modify a customer's billing address of this order And I specify their new billing address as "Los Angeles", "Seaside Fwy", "90802", "United States" for "Lucifer Morningstar" And I save my changes Then I should be notified that it has been successfully edited - And this order bill should be shipped to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" + And this order should have "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" as its billing address - @ui + @api @ui Scenario: Modifying a customer's billing address when a product's price has been changed Given the product "Suit" changed its price to "$300.00" When I view the summary of the order "#00000001" @@ -32,10 +32,10 @@ Feature: Modifying a customer billing address after an order has been placed And I specify their new billing address as "Los Angeles", "Seaside Fwy", "90802", "United States" for "Lucifer Morningstar" And I save my changes Then I should be notified that it has been successfully edited - And this order bill should be shipped to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" + And this order should have "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" as its billing address And the order's total should still be "$400.00" - @ui + @api @ui Scenario: Modifying a customer's billing address when a channel has been disabled Given the channel "Web" has been disabled When I view the summary of the order "#00000001" @@ -43,10 +43,10 @@ Feature: Modifying a customer billing address after an order has been placed And I specify their new billing address as "Los Angeles", "Seaside Fwy", "90802", "United States" for "Lucifer Morningstar" And I save my changes Then I should be notified that it has been successfully edited - And this order bill should be shipped to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" + And this order should have "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" as its billing address And the order's total should still be "$400.00" - @ui + @api @ui Scenario: Modifying a customer's billing address when the currency has been disabled Given the currency "USD" has been disabled When I view the summary of the order "#00000001" @@ -54,10 +54,10 @@ Feature: Modifying a customer billing address after an order has been placed And I specify their new billing address as "Los Angeles", "Seaside Fwy", "90802", "United States" for "Lucifer Morningstar" And I save my changes Then I should be notified that it has been successfully edited - And this order bill should be shipped to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" + And this order should have "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" as its billing address And the order's total should still be "$400.00" - @ui + @api @ui Scenario: Modifying a customer's billing address when the product is out of stock Given the product "Suit" is out of stock When I view the summary of the order "#00000001" @@ -65,5 +65,5 @@ Feature: Modifying a customer billing address after an order has been placed And I specify their new billing address as "Los Angeles", "Seaside Fwy", "90802", "United States" for "Lucifer Morningstar" And I save my changes Then I should be notified that it has been successfully edited - And this order bill should be shipped to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" + And this order should have "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" as its billing address And the order's total should still be "$400.00" diff --git a/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_coupon.feature b/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_coupon.feature index 146f943109..b586299a8f 100644 --- a/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_coupon.feature +++ b/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_coupon.feature @@ -1,4 +1,4 @@ -@modifying_address +@modifying_placed_order_address Feature: Modifying a customer's billing address on an order with an applied coupon In order to ship an order's bill to a correct place As an Administrator @@ -19,7 +19,7 @@ Feature: Modifying a customer's billing address on an order with an applied coup And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Modifying a customer's billing address when the applied coupon is no longer valid Given the coupon "HOLIDAY" was used up to its usage limit When I view the summary of the order "#00000001" @@ -27,6 +27,6 @@ Feature: Modifying a customer's billing address on an order with an applied coup And I specify their new billing address as "Los Angeles", "Seaside Fwy", "90802", "United States" for "Lucifer Morningstar" And I save my changes Then I should be notified that it has been successfully edited - And the order should be billed to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" + And this order should have "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" as its billing address And the order's total should still be "$350.00" And the order's promotion total should still be "-$50.00" diff --git a/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_promotion.feature b/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_promotion.feature index 61ffc81ccf..d6b57a2ff2 100644 --- a/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_promotion.feature +++ b/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_promotion.feature @@ -1,4 +1,4 @@ -@modifying_address +@modifying_placed_order_address Feature: Modifying a customer's billing address on an order with an applied promotion In order to ship an order's bill to a correct place As an Administrator @@ -19,14 +19,14 @@ Feature: Modifying a customer's billing address on an order with an applied prom And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui - Scenario: Modifying a customer's billing address when the applied promotion is no longer valid + @api @ui + Scenario: Modifying a customer's billing address of already placed order when the applied promotion is no longer valid Given the promotion was disabled for the channel "Web" When I view the summary of the order "#00000001" And I want to modify a customer's billing address of this order And I specify their new billing address as "Los Angeles", "Seaside Fwy", "90802", "United States" for "Lucifer Morningstar" And I save my changes Then I should be notified that it has been successfully edited - And the order should be billed to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" + And this order should have "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" as its billing address And the order's total should still be "$350.00" And the order's promotion total should still be "-$50.00" diff --git a/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_taxes.feature b/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_taxes.feature index 25bce3954b..98c7e3d222 100644 --- a/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_taxes.feature +++ b/features/order/managing_orders/modifying_billing_address/modifying_billing_address_on_order_with_taxes.feature @@ -1,4 +1,4 @@ -@modifying_address +@modifying_placed_order_address Feature: Modifying a customer's billing address on an order with taxes In order to ship an order's bill to a correct place As an Administrator @@ -17,14 +17,14 @@ Feature: Modifying a customer's billing address on an order with taxes And the customer chose "Free" shipping method with "Offline" payment And I am logged in as an administrator - @ui - Scenario: Modifying a customer's billing address when the applied promotion is no longer valid + @api @ui + Scenario: Modifying a customer's billing address of already placed order after the VAT tax rate has been changed Given the "VAT" tax rate has changed to 10% When I view the summary of the order "#00000001" And I want to modify a customer's billing address of this order And I specify their new billing address as "Los Angeles", "Seaside Fwy", "90802", "United States" for "Lucifer Morningstar" And I save my changes Then I should be notified that it has been successfully edited - And the order should be billed to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" + And this order should have "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" as its billing address And the order's total should still be "$480.00" And the order's tax total should still be "$80.00" diff --git a/features/order/managing_orders/modifying_billing_address/modifying_billing_address_validation.feature b/features/order/managing_orders/modifying_billing_address/modifying_billing_address_validation.feature index 5bd5b81cdb..6e6ffce0aa 100644 --- a/features/order/managing_orders/modifying_billing_address/modifying_billing_address_validation.feature +++ b/features/order/managing_orders/modifying_billing_address/modifying_billing_address_validation.feature @@ -1,4 +1,4 @@ -@modifying_address +@modifying_placed_order_address Feature: Modifying a customer's billing address validation In order to avoid making mistakes when modifying a customer's billing address As an Administrator @@ -16,12 +16,12 @@ Feature: Modifying a customer's billing address validation And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui - Scenario: Address an order without name, city and street + @api @ui + Scenario: Attempt to save an order with incomplete billing address When I view the summary of the order "#00000001" And I want to modify a customer's billing address of this order - And I clear old billing address information + And I clear the billing address information But I do not specify new information And I try to save my changes - Then I should be notified that the "first name", the "last name", the "city" and the "street" in billing details are required - And this order bill should still be shipped to "Mike Ross", "350 5th Ave", "10118", "New York", "United States" + Then I should be notified that all mandatory billing address details are incomplete + And this order should still have "Mike Ross", "350 5th Ave", "10118", "New York", "United States" as its billing address diff --git a/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address.feature b/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address.feature index e62edc12f2..a28c49c659 100644 --- a/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address.feature +++ b/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address.feature @@ -1,4 +1,4 @@ -@modifying_address +@modifying_placed_order_address Feature: Modifying a customer shipping address after an order has been placed In order to ship an order to a correct place As an Administrator @@ -16,7 +16,7 @@ Feature: Modifying a customer shipping address after an order has been placed And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Modifying a customer's shipping address When I view the summary of the order "#00000001" And I want to modify a customer's shipping address of this order @@ -25,7 +25,7 @@ Feature: Modifying a customer shipping address after an order has been placed Then I should be notified that it has been successfully edited And this order should be shipped to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" - @ui + @api @ui Scenario: Modifying a customer's shipping address when a product's price has been changed Given the product "Suit" changed its price to "$300.00" When I view the summary of the order "#00000001" @@ -36,7 +36,7 @@ Feature: Modifying a customer shipping address after an order has been placed And this order should be shipped to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" And the order's total should still be "$400.00" - @ui + @api @ui Scenario: Modifying a customer's shipping address when a channel has been disabled Given the channel "Web" has been disabled When I view the summary of the order "#00000001" @@ -47,7 +47,7 @@ Feature: Modifying a customer shipping address after an order has been placed And this order should be shipped to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" And the order's total should still be "$400.00" - @ui + @api @ui Scenario: Modifying a customer's shipping address when the currency has been disabled Given the currency "USD" has been disabled When I view the summary of the order "#00000001" @@ -58,7 +58,7 @@ Feature: Modifying a customer shipping address after an order has been placed And this order should be shipped to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" And the order's total should still be "$400.00" - @ui + @api @ui Scenario: Modifying a customer's shipping address when the product is out of stock Given the product "Suit" is out of stock When I view the summary of the order "#00000001" diff --git a/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_coupon.feature b/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_coupon.feature index a01ff1887a..9385e1fd34 100644 --- a/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_coupon.feature +++ b/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_coupon.feature @@ -1,4 +1,4 @@ -@modifying_address +@modifying_placed_order_address Feature: Modifying a customer's shipping address on an order with an applied coupon In order to ship an order to a correct place As an Administrator @@ -20,7 +20,7 @@ Feature: Modifying a customer's shipping address on an order with an applied cou And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Modifying a customer's shipping address when the applied coupon is no longer valid Given the coupon "HOLIDAY" was used up to its usage limit When I view the summary of the order "#00000001" diff --git a/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_promotion.feature b/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_promotion.feature index 4b696dbdad..f7d816ba76 100644 --- a/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_promotion.feature +++ b/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_promotion.feature @@ -1,4 +1,4 @@ -@modifying_address +@modifying_placed_order_address Feature: Modifying a customer's shipping address on an order with an applied promotion In order to ship an order to a correct place As an Administrator @@ -20,8 +20,8 @@ Feature: Modifying a customer's shipping address on an order with an applied pro And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui - Scenario: Modifying a customer's shipping address when the applied promotion is no longer valid + @api @ui + Scenario: Modifying a customer's shipping address of already placed order when the applied promotion is no longer valid Given the promotion was disabled for the channel "Web" When I view the summary of the order "#00000001" And I want to modify a customer's shipping address of this order diff --git a/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_taxes.feature b/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_taxes.feature index b3e14c2e5b..6a4c3b2e7e 100644 --- a/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_taxes.feature +++ b/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_on_order_with_taxes.feature @@ -1,4 +1,4 @@ -@modifying_address +@modifying_placed_order_address Feature: Modifying a customer's shipping address on an order with taxes In order to ship an order to a correct place As an Administrator @@ -18,8 +18,8 @@ Feature: Modifying a customer's shipping address on an order with taxes And the customer chose "Free" shipping method with "Offline" payment And I am logged in as an administrator - @ui - Scenario: Modifying a customer's shipping address when the applied promotion is no longer valid + @api @ui + Scenario: Modifying a customer's shipping address of already placed order after the VAT tax rate has been changed Given the "VAT" tax rate has changed to 10% When I view the summary of the order "#00000001" And I want to modify a customer's shipping address of this order diff --git a/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_validation.feature b/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_validation.feature index cc22017744..1d13f84b6c 100644 --- a/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_validation.feature +++ b/features/order/managing_orders/modifying_shipping_address/modifying_shipping_address_validation.feature @@ -1,4 +1,4 @@ -@modifying_address +@modifying_placed_order_address Feature: Modifying a customer's shipping address validation In order to avoid making mistakes when modifying a customer's shipping address As an Administrator @@ -16,12 +16,12 @@ Feature: Modifying a customer's shipping address validation And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui - Scenario: Address an order without name, city and street + @api @ui + Scenario: Attempt to save an order with incomplete shipping address When I view the summary of the order "#00000001" And I want to modify a customer's shipping address of this order - And I clear old shipping address information + And I clear the shipping address information But I do not specify new information And I try to save my changes - Then I should be notified that the "first name", the "last name", the "city" and the "street" in shipping details are required + Then I should be notified that all mandatory shipping address details are incomplete And this order should still be shipped to "Mike Ross", "350 5th Ave", "10118", "New York", "United States" diff --git a/features/order/managing_orders/order_details/being_unable_to_see_cart.feature b/features/order/managing_orders/order_details/being_unable_to_see_cart.feature index 92e846a0dd..a88a25b0a8 100644 --- a/features/order/managing_orders/order_details/being_unable_to_see_cart.feature +++ b/features/order/managing_orders/order_details/being_unable_to_see_cart.feature @@ -10,7 +10,7 @@ Feature: Being unable to see details of a cart And the customer added "PHP T-Shirt" product to the cart And I am logged in as an administrator - @ui - Scenario: Seeing basic information about an order + @ui @api + Scenario: Being unable to see the details of a cart When I try to view the summary of the customer's latest cart Then I should be informed that the order does not exist diff --git a/features/order/managing_orders/order_details/seeing_order.feature b/features/order/managing_orders/order_details/seeing_order.feature index b882bd9c6a..7c16f57baa 100644 --- a/features/order/managing_orders/order_details/seeing_order.feature +++ b/features/order/managing_orders/order_details/seeing_order.feature @@ -16,11 +16,11 @@ Feature: Seeing basic information about an order And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing basic information about an order When I view the summary of the order "#00000666" Then it should have been placed by the customer "lucy@teamlucifer.com" And it should be shipped to "Lucifer Morningstar", "Seaside Fwy", "90802", "Los Angeles", "United States" - And it should be billed to "Mazikeen Lilim", "Pacific Coast Hwy", "90806", "Los Angeles", "United States" + And it should have "Mazikeen Lilim", "Pacific Coast Hwy", "90806", "Los Angeles", "United States" as its billing address And it should be shipped via the "Free" shipping method And it should be paid with "Cash on Delivery" diff --git a/features/order/managing_orders/order_details/seeing_order_aggregated_discounts.feature b/features/order/managing_orders/order_details/seeing_order_aggregated_discounts.feature index 9d5028dd1a..6dccd07970 100644 --- a/features/order/managing_orders/order_details/seeing_order_aggregated_discounts.feature +++ b/features/order/managing_orders/order_details/seeing_order_aggregated_discounts.feature @@ -16,7 +16,7 @@ Feature: Seeing aggregated discounts of an order And there is a customer "robin.hood@sherwood.com" that placed an order "#00000006" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing shipping and order promotions, but the shipping promotion is not aggregated in summary's promotion total Given the customer bought 2 "Longbow" products And the customer chose "DHL" shipping method to "United States" with "Cash on Delivery" payment @@ -28,7 +28,7 @@ Feature: Seeing aggregated discounts of an order And the order's shipping total should be "$5.00" And the order's total should be "$285.00" - @ui + @api @ui Scenario: Seeing multiple order promotions aggregated in summary Given there is a promotion "Big order discount" And it gives "$70.00" discount to every order with quantity at least 3 diff --git a/features/order/managing_orders/order_details/seeing_order_aggregated_taxes.feature b/features/order/managing_orders/order_details/seeing_order_aggregated_taxes.feature index ca49b0fd83..07a5a756a6 100644 --- a/features/order/managing_orders/order_details/seeing_order_aggregated_taxes.feature +++ b/features/order/managing_orders/order_details/seeing_order_aggregated_taxes.feature @@ -20,7 +20,7 @@ Feature: Seeing aggregated taxes of an order And there is a customer "charles.the.great@medieval.com" that placed an order "#00000001" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing aggregated taxes of products and shipping Given the customer bought a single "Composite bow" And the customer chose "DHL" shipping method to "United States" with "Offline" payment @@ -31,7 +31,7 @@ Feature: Seeing aggregated taxes of an order And the order's tax total should be "$25.30" And the order's total should be "$135.30" - @ui + @api @ui Scenario: Seeing aggregated taxes of multiple products from different tax rates and shipping Given the customer bought a single "Composite bow" And the customer bought a "Claymore" and a "Bastard sword" diff --git a/features/order/managing_orders/order_details/seeing_order_currency_on_show.feature b/features/order/managing_orders/order_details/seeing_order_currency_on_show.feature index 11e109949d..49c8c6ee23 100644 --- a/features/order/managing_orders/order_details/seeing_order_currency_on_show.feature +++ b/features/order/managing_orders/order_details/seeing_order_currency_on_show.feature @@ -22,7 +22,7 @@ Feature: Seeing the currency an order has been placed in on it's details page And there is a customer "Satin" identified by an email "satin@teamlucifer.com" and a password "pswd" And I am logged in as an administrator - @ui + @api @ui Scenario: All prices are in the base currency when the client haven't chosen any other Given there is a customer "satin@teamlucifer.com" that placed an order "#00000666" And the customer bought a single "Angel T-Shirt" @@ -31,14 +31,14 @@ Feature: Seeing the currency an order has been placed in on it's details page And the customer chose "DHL" shipping method with "Cash on Delivery" payment When I view the summary of the order "#00000666" And I check "Angel T-Shirt" data - Then its discounted unit price should be $5.00 - And its unit price should be $20.00 - And its subtotal should be $5.00 - And its discount should be -$10.00 - And its tax should be $0.50 - And its total should be $5.50 - And the order's items total should be "$5.50" + Then the order's items total should be "$5.50" And the order's shipping total should be "$20.00" And the order's tax total should be "$0.50" And the order's promotion total should be "-$15.00" And the order's total should be "$25.50" + And its unit price should be $20.00 + And its total should be $5.50 + And its discounted unit price should be $5.00 + And its subtotal should be $5.00 + And its discount should be -$10.00 + And its tax should be $0.50 diff --git a/features/order/managing_orders/order_details/seeing_order_discounts.feature b/features/order/managing_orders/order_details/seeing_order_discounts.feature index a12e318b9e..0334feb27f 100644 --- a/features/order/managing_orders/order_details/seeing_order_discounts.feature +++ b/features/order/managing_orders/order_details/seeing_order_discounts.feature @@ -15,7 +15,7 @@ Feature: Seeing discounts of an order And there is a customer "lucy@teamlucifer.com" that placed an order "#00000666" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing promotion discount on order while buying at least 3 items Given the promotion gives "$15.00" discount to every order with quantity at least 3 And the customer bought 4 "Angel T-Shirt" products @@ -26,7 +26,7 @@ Feature: Seeing discounts of an order And the order's promotion discount should be "-$15.00" from "Holiday promotion" promotion And the order's total should be "$141.00" - @ui + @api @ui Scenario: Seeing promotion discount on order's items while buying a product from a promoted taxon Given the promotion gives "$10.00" off on every product classified as "T-Shirts" And the customer bought a single "Angel T-Shirt" diff --git a/features/order/managing_orders/order_details/seeing_order_item_detailed_data.feature b/features/order/managing_orders/order_details/seeing_order_item_detailed_data.feature index 15c296d3a7..d3c3bf7c8d 100644 --- a/features/order/managing_orders/order_details/seeing_order_item_detailed_data.feature +++ b/features/order/managing_orders/order_details/seeing_order_item_detailed_data.feature @@ -23,14 +23,14 @@ Feature: Seeing order item detailed data And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing details of item in one row Given I view the summary of the order "#00000666" - When I check "Iron Man T-Shirt" data + When I check "Marvel T-Shirt" data Then its code should be "IRON_MAN_T_SHIRT" And its unit price should be $49.00 - And its discounted unit price should be $44.00 + And its total should be $193.60 And its quantity should be 4 + And its discounted unit price should be $44.00 And its subtotal should be $176.00 And its tax should be $17.60 - And its total should be $193.60 diff --git a/features/order/managing_orders/order_details/seeing_order_item_included_taxes.feature b/features/order/managing_orders/order_details/seeing_order_item_included_taxes.feature index d9a17abab1..5decf0c51f 100644 --- a/features/order/managing_orders/order_details/seeing_order_item_included_taxes.feature +++ b/features/order/managing_orders/order_details/seeing_order_item_included_taxes.feature @@ -15,12 +15,12 @@ Feature: Seeing included in price taxes of order items And there is a customer "lucy@teamlucifer.com" that placed an order "#00000666" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing included in price taxes of order items are not counted in taxes total Given the customer bought 2 "Winchester M1866" products And the customer chose "Free" shipping method to "United States" with "Offline" payment When I view the summary of the order "#00000666" And I check "Winchester M1866" data - Then its tax included in price should be $40.00 - And the order's tax total should be "$40.00" + Then the order's tax total should be "$40.00" And the order's total should be "$440.00" + And its tax included in price should be $40.00 diff --git a/features/order/managing_orders/order_details/seeing_order_item_taxes.feature b/features/order/managing_orders/order_details/seeing_order_item_taxes.feature index f538a6ee54..5eb3c596bb 100644 --- a/features/order/managing_orders/order_details/seeing_order_item_taxes.feature +++ b/features/order/managing_orders/order_details/seeing_order_item_taxes.feature @@ -15,7 +15,7 @@ Feature: Seeing taxes of order items And there is a customer "lucy@teamlucifer.com" that placed an order "#00000666" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing taxes of order items Given the customer bought a "PHP T-Shirt" and a "Symfony2 T-Shirt" And the customer chose "Free" shipping method to "United States" with "Offline" payment diff --git a/features/order/managing_orders/order_details/seeing_order_items_with_proper_names.feature b/features/order/managing_orders/order_details/seeing_order_items_with_proper_names.feature index 620b7dd0b6..24cc2ca4d1 100644 --- a/features/order/managing_orders/order_details/seeing_order_items_with_proper_names.feature +++ b/features/order/managing_orders/order_details/seeing_order_items_with_proper_names.feature @@ -15,7 +15,7 @@ Feature: Seeing order items with proper names And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing order items with proper names Given the product "Angel T-Shirt" was renamed to "Devil Cardigan" And the product "Angel Mug" was renamed to "Devil Glass" diff --git a/features/order/managing_orders/order_details/seeing_order_new_state_after_customers_checkout.feature b/features/order/managing_orders/order_details/seeing_order_new_state_after_customers_checkout.feature index b34d15e899..ca8313c618 100644 --- a/features/order/managing_orders/order_details/seeing_order_new_state_after_customers_checkout.feature +++ b/features/order/managing_orders/order_details/seeing_order_new_state_after_customers_checkout.feature @@ -12,7 +12,7 @@ Feature: Placing an order And there is a customer "sylius@example.com" that placed an order "#00000022" And I am logged in as an administrator - @ui + @api @ui Scenario: Verifying that order has new state right after checkout Given the customer bought 3 "Iron Maiden T-Shirt" products And the customer chose "Free" shipping method to "United States" with "Offline" payment diff --git a/features/order/managing_orders/order_details/seeing_order_payment_state_as_completed_if_free_order_coupon_was_applied.feature b/features/order/managing_orders/order_details/seeing_order_payment_state_as_completed_if_free_order_coupon_was_applied.feature index e44bea3a22..95d4d9b147 100644 --- a/features/order/managing_orders/order_details/seeing_order_payment_state_as_completed_if_free_order_coupon_was_applied.feature +++ b/features/order/managing_orders/order_details/seeing_order_payment_state_as_completed_if_free_order_coupon_was_applied.feature @@ -18,7 +18,7 @@ Feature: Seeing payment state as paid after checkout steps if order total is zer And the customer chose "Free" shipping method And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing payment state as paid on orders list When I browse orders Then the order "#00000666" should have order payment state "Paid" diff --git a/features/order/managing_orders/order_details/seeing_order_payment_state_as_completed_if_order_total_is_zero.feature b/features/order/managing_orders/order_details/seeing_order_payment_state_as_completed_if_order_total_is_zero.feature index 5bdeb1bdf0..fcc8a32250 100644 --- a/features/order/managing_orders/order_details/seeing_order_payment_state_as_completed_if_order_total_is_zero.feature +++ b/features/order/managing_orders/order_details/seeing_order_payment_state_as_completed_if_order_total_is_zero.feature @@ -16,12 +16,17 @@ Feature: Seeing payment state as paid after checkout steps if order total is zer And the customer chose "Free" shipping method And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing payment state as paid on orders list When I browse orders Then the order "#00000666" should have order payment state "Paid" - @ui + @api @no-ui + Scenario: Seeing payment state as paid on order's summary + When I view the summary of the order "#00000666" + Then I should be informed that there are no payments + + @ui @no-api Scenario: Seeing payment state as paid on order's summary When I view the summary of the order "#00000666" Then I should be informed that there are no payments diff --git a/features/order/managing_orders/order_details/seeing_order_shipment_state_after_checkout.feature b/features/order/managing_orders/order_details/seeing_order_shipment_state_after_checkout.feature index d28bd14aaf..c97a1e025a 100644 --- a/features/order/managing_orders/order_details/seeing_order_shipment_state_after_checkout.feature +++ b/features/order/managing_orders/order_details/seeing_order_shipment_state_after_checkout.feature @@ -15,19 +15,19 @@ Feature: Seeing shipping states of an order after checkout steps And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing ready order shipping state When I browse orders - Then order "#00000666" should have shipment state ready + Then order "#00000666" should have shipment state "ready" - @ui + @api @ui Scenario: Seeing shipped order shipping state Given this order has already been shipped When I browse orders - Then order "#00000666" should have shipment state shipped + Then order "#00000666" should have shipment state "shipped" - @ui + @api @ui Scenario: Seeing cancelled order shipping state Given the customer cancelled this order When I browse orders - Then order "#00000666" should have shipment state cancelled + Then order "#00000666" should have shipment state "cancelled" diff --git a/features/order/managing_orders/order_details/seeing_order_shipment_state_as_shipped_if_there_is_not_shipments_to_deliver.feature b/features/order/managing_orders/order_details/seeing_order_shipment_state_as_shipped_if_there_is_not_shipments_to_deliver.feature index 8bd9c55342..2d3201e533 100644 --- a/features/order/managing_orders/order_details/seeing_order_shipment_state_as_shipped_if_there_is_not_shipments_to_deliver.feature +++ b/features/order/managing_orders/order_details/seeing_order_shipment_state_as_shipped_if_there_is_not_shipments_to_deliver.feature @@ -16,12 +16,12 @@ Feature: Seeing shipping states of an order as shipped if there are no shipments And the customer chose "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing shipping state as shipped on orders list When I browse orders Then the order "#00000666" should have order shipping state "Shipped" - @ui + @api @ui Scenario: Seeing shipping state as shipped on order's summary When I view the summary of the order "#00000666" Then it should have order's shipping state "Shipped" diff --git a/features/order/managing_orders/order_details/seeing_order_shipping_fees.feature b/features/order/managing_orders/order_details/seeing_order_shipping_fees.feature index 02ce136622..12222dd7ee 100644 --- a/features/order/managing_orders/order_details/seeing_order_shipping_fees.feature +++ b/features/order/managing_orders/order_details/seeing_order_shipping_fees.feature @@ -14,7 +14,7 @@ Feature: Seeing shipping fees of an order And the customer bought a single "Angel T-Shirt" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing Free shipping of an order Given the customer chose "Free" shipping method to "United States" with "Offline" payment When I view the summary of the order "#00000666" @@ -24,7 +24,7 @@ Feature: Seeing shipping fees of an order And the order's shipping total should be "$0.00" And the order's total should be "$39.00" - @ui + @api @ui Scenario: Seeing shipping fee of an order Given the customer chose "DHL" shipping method to "United States" with "Offline" payment When I view the summary of the order "#00000666" diff --git a/features/order/managing_orders/order_details/seeing_order_shipping_taxes.feature b/features/order/managing_orders/order_details/seeing_order_shipping_taxes.feature index f4a9f8184d..674116b518 100644 --- a/features/order/managing_orders/order_details/seeing_order_shipping_taxes.feature +++ b/features/order/managing_orders/order_details/seeing_order_shipping_taxes.feature @@ -16,7 +16,7 @@ Feature: Seeing taxes of an order And there is a customer "lucy@teamlucifer.com" that placed an order "#00000666" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing taxes of order items and shipping Given the customer bought a single "Symfony2 T-Shirt" And the customer chose "DHL" shipping method to "United States" with "Offline" payment @@ -28,7 +28,7 @@ Feature: Seeing taxes of an order And the order's tax total should be "$34.50" And the order's total should be "$184.50" - @ui + @api @ui Scenario: Seeing taxes of items and shipping from paid order Given the customer bought a single "Symfony2 T-Shirt" And the customer chose "DHL" shipping method to "United States" with "Offline" payment diff --git a/features/order/managing_orders/order_details/seeing_order_shipping_total_with_applied_promotion_and_taxes.feature b/features/order/managing_orders/order_details/seeing_order_shipping_total_with_applied_promotion_and_taxes.feature index 3182fbe113..1e4d2d9ea8 100644 --- a/features/order/managing_orders/order_details/seeing_order_shipping_total_with_applied_promotion_and_taxes.feature +++ b/features/order/managing_orders/order_details/seeing_order_shipping_total_with_applied_promotion_and_taxes.feature @@ -19,7 +19,7 @@ Feature: Seeing shipping total with applied promotion and taxes And the customer bought a single "Gryffindor scarf" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing taxes of order items and shipping with applied promotion Given the customer chose "Owl post" shipping method to "United States" with "Offline" payment When I view the summary of the order "#00000777" @@ -30,7 +30,7 @@ Feature: Seeing shipping total with applied promotion and taxes And the order's tax total should be "$24.15" And the order's total should be "$129.15" - @ui @domain + @api @ui @domain Scenario: Seeing taxes of order items and multiple shipments with applied promotion Given the store has "Pigeon post" shipping method with "$16.00" fee within the "US" zone And shipping method "Pigeon post" belongs to "Shipping Services" tax category diff --git a/features/order/managing_orders/order_details/seeing_order_with_different_promotions.feature b/features/order/managing_orders/order_details/seeing_order_with_different_promotions.feature index c445b117ed..90b6d0e780 100644 --- a/features/order/managing_orders/order_details/seeing_order_with_different_promotions.feature +++ b/features/order/managing_orders/order_details/seeing_order_with_different_promotions.feature @@ -17,21 +17,21 @@ Feature: Seeing order with different promotions And there is a customer "lucy@teamlucifer.com" that placed an order "#00000666" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing prices and discount prices of order item Given the customer bought 2 "PHP T-Shirt" products And the customer bought a single "Symfony Mug" And the customer chose "Free" shipping method to "United States" with "Offline" payment When I view the summary of the order "#00000666" Then the "PHP T-Shirt" product's unit price should be "$60.00" - And the "PHP T-Shirt" product's item discount should be "-$20.00" - And the "PHP T-Shirt" product's order discount should be "~ -$3.34" And the "PHP T-Shirt" product's discounted unit price should be "$36.66" And the "PHP T-Shirt" product's quantity should be 2 + And the "PHP T-Shirt" product's item discount should be "-$20.00" + And the "PHP T-Shirt" product's order discount should be "~ -$3.34" And the "PHP T-Shirt" product's subtotal should be "$73.33" And the "Symfony Mug" product's unit price should be "$40.00" - And the "Symfony Mug" product's item discount should be "$0.00" - And the "Symfony Mug" product's order discount should be "~ -$3.33" And the "Symfony Mug" product's discounted unit price should be "$36.67" And the "Symfony Mug" product's quantity should be 1 + And the "Symfony Mug" product's item discount should be "$0.00" + And the "Symfony Mug" product's order discount should be "~ -$3.33" And the "Symfony Mug" product's subtotal should be "$36.67" diff --git a/features/order/managing_orders/order_details/seeing_order_with_items.feature b/features/order/managing_orders/order_details/seeing_order_with_items.feature index e720a348ac..3d87782ae6 100644 --- a/features/order/managing_orders/order_details/seeing_order_with_items.feature +++ b/features/order/managing_orders/order_details/seeing_order_with_items.feature @@ -15,7 +15,7 @@ Feature: Seeing an order with its items And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing order items When I view the summary of the order "#00000666" Then it should have 2 items diff --git a/features/order/managing_orders/order_details/seeing_order_without_shipping_address.feature b/features/order/managing_orders/order_details/seeing_order_without_shipping_address.feature index 10c79ff59f..a6c1513e8f 100644 --- a/features/order/managing_orders/order_details/seeing_order_without_shipping_address.feature +++ b/features/order/managing_orders/order_details/seeing_order_without_shipping_address.feature @@ -16,10 +16,10 @@ Feature: Seeing an order without shipping address And the customer chose "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing basic information about an order When I view the summary of the order "#00000666" Then it should have been placed by the customer "lucy@teamlucifer.com" - And it should be billed to "Mike Ross", "350 5th Ave", "10118", "New York", "United States" + And it should have "Mike Ross", "350 5th Ave", "10118", "New York", "United States" as its billing address And it should be paid with "Cash on Delivery" And it should have no shipping address set diff --git a/features/order/managing_orders/order_details/seeing_province_created_manually_on_order_page.feature b/features/order/managing_orders/order_details/seeing_province_created_manually_on_order_page.feature index b035062a5d..eab25f8a0e 100644 --- a/features/order/managing_orders/order_details/seeing_province_created_manually_on_order_page.feature +++ b/features/order/managing_orders/order_details/seeing_province_created_manually_on_order_page.feature @@ -19,8 +19,8 @@ Feature: Seeing province created manually on order summary page And the customer chose "DHL" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing manually definied province on order summary page When I view the summary of the order "#00000666" Then I should see "East of England" as province in the shipping address - And I should see "East of England" ad province in the billing address + And I should see "East of England" as province in the billing address diff --git a/features/order/managing_orders/order_details/seeing_shipment_shipping_date.feature b/features/order/managing_orders/order_details/seeing_shipment_shipping_date.feature index e62f5d523b..104606918e 100644 --- a/features/order/managing_orders/order_details/seeing_shipment_shipping_date.feature +++ b/features/order/managing_orders/order_details/seeing_shipment_shipping_date.feature @@ -15,7 +15,7 @@ Feature: Seeing shipment shipping date And it is "20-02-2020 10:30:05" now And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing shipped at date When I view the summary of the order "#00000777" And I ship this order diff --git a/features/order/managing_orders/order_filtration/filtering_orders_by_channel.feature b/features/order/managing_orders/order_filtration/filtering_orders_by_channel.feature index 97ef558a16..8e38ba6a20 100644 --- a/features/order/managing_orders/order_filtration/filtering_orders_by_channel.feature +++ b/features/order/managing_orders/order_filtration/filtering_orders_by_channel.feature @@ -13,7 +13,7 @@ Feature: Filtering orders by a channel And this customer has also placed an order "#00000003" on a channel "Web-US" And I am logged in as an administrator - @ui + @ui @api Scenario: Filtering orders by a chosen channel When I browse orders And I choose "Web-EU" as a channel filter @@ -23,7 +23,7 @@ Feature: Filtering orders by a channel And I should see an order with "#00000002" number But I should not see an order with "#00000003" number - @ui + @ui @api Scenario: Filtering orders by an another channel When I browse orders And I choose "Web-US" as a channel filter diff --git a/features/order/managing_orders/order_filtration/filtering_orders_by_date.feature b/features/order/managing_orders/order_filtration/filtering_orders_by_date.feature index 0d8324b199..e6b39dd5ab 100644 --- a/features/order/managing_orders/order_filtration/filtering_orders_by_date.feature +++ b/features/order/managing_orders/order_filtration/filtering_orders_by_date.feature @@ -12,7 +12,7 @@ Feature: Filtering orders And this customer has also placed an order "#00000003" at "2016-12-06 10:00" And I am logged in as an administrator - @ui + @ui @api Scenario: Filtering orders by date from When I browse orders And I specify filter date from as "2016-12-05 08:30" @@ -22,7 +22,7 @@ Feature: Filtering orders And I should see an order with "#00000003" number But I should not see an order with "#00000001" number - @ui + @ui @api Scenario: Filtering orders by date to When I browse orders And I specify filter date to as "2016-12-05 09:30" @@ -32,7 +32,7 @@ Feature: Filtering orders And I should see an order with "#00000002" number But I should not see an order with "#00000003" number - @ui + @ui @api Scenario: Filtering orders by date from to When I browse orders And I specify filter date from as "2016-12-05 08:30" @@ -43,25 +43,25 @@ Feature: Filtering orders But I should not see an order with "#00000001" number And I should not see an order with "#00000003" number - @ui + @ui @api Scenario: Filtering orders by date from without time When I browse orders And I specify filter date from as "2016-12-05" - And I specify filter date to as "2016-12-06" + And I specify filter date to as "2016-12-07" And I filter Then I should see 2 orders in the list And I should see an order with "#00000002" number And I should see an order with "#00000003" number But I should not see an order with "#00000001" number - @ui + @ui @api Scenario: Filtering orders placed at midnight or just before midnight Given this customer has placed an order "#00000004" at "2016-12-10 00:00" And this customer has also placed an order "#00000005" at "2016-12-11 23:59" And this customer has also placed an order "#00000006" at "2016-12-12 00:00" When I browse orders And I specify filter date from as "2016-12-10" - And I specify filter date to as "2016-12-11" + And I specify filter date to as "2016-12-11 23:59" And I filter Then I should see 2 orders in the list And I should see an order with "#00000004" number diff --git a/features/order/managing_orders/order_filtration/filtering_orders_by_products.feature b/features/order/managing_orders/order_filtration/filtering_orders_by_products.feature index dafb77ff91..f998fdf220 100644 --- a/features/order/managing_orders/order_filtration/filtering_orders_by_products.feature +++ b/features/order/managing_orders/order_filtration/filtering_orders_by_products.feature @@ -17,7 +17,7 @@ Feature: Filtering orders by products And the customer chose "Free" shipping method to "United States" with "Offline" payment And I am logged in as an administrator - @ui @javascript + @ui @api @javascript Scenario: Filtering orders by product When I browse orders And I filter by product "Galaxy T-Shirt" @@ -25,7 +25,7 @@ Feature: Filtering orders by products And I should see an order with "#0000001" number And I should see an order with "#0000003" number - @ui @mink:chromedriver + @ui @api @mink:chromedriver Scenario: Filtering orders by multiple products When I browse orders And I filter by products "Galaxy T-Shirt" and "Space Dress" diff --git a/features/order/managing_orders/order_filtration/filtering_orders_by_shipping_method.feature b/features/order/managing_orders/order_filtration/filtering_orders_by_shipping_method.feature index 68bc72f8c3..23ea147262 100644 --- a/features/order/managing_orders/order_filtration/filtering_orders_by_shipping_method.feature +++ b/features/order/managing_orders/order_filtration/filtering_orders_by_shipping_method.feature @@ -23,7 +23,7 @@ Feature: Filtering orders by a shipping method And the customer chose "DHL" shipping method to "United States" with "Offline" payment And I am logged in as an administrator - @ui + @ui @api Scenario: Filtering orders by DHL shipping method When I browse orders And I choose "DHL" as a shipping method filter @@ -32,7 +32,7 @@ Feature: Filtering orders by a shipping method And I should see an order with "#000001338" number And I should not see an order with "#000001337" number - @ui + @ui @api Scenario: Filtering orders by an another shipping method When I browse orders And I choose "Free" as a shipping method filter diff --git a/features/order/managing_orders/order_filtration/filtering_orders_by_total_in_different_currencies.feature b/features/order/managing_orders/order_filtration/filtering_orders_by_total_in_different_currencies.feature index a9fb895296..4fa36f9b1f 100644 --- a/features/order/managing_orders/order_filtration/filtering_orders_by_total_in_different_currencies.feature +++ b/features/order/managing_orders/order_filtration/filtering_orders_by_total_in_different_currencies.feature @@ -9,23 +9,23 @@ Feature: Filtering orders by total in different currencies And the store operates on a channel named "Web-UK" in "GBP" currency And the store has "Apple T-Shirt", "Pineapple T-Shirt" and "Pen T-Shirt" products And the store has customer "John Hancock" with email "hancock@superheronope.com" - And this customer has placed an order "#00000001" buying a single "Apple T-Shirt" product for "$100" on the "Web-US" channel - And this customer has also placed an order "#00000002" buying a single "Pineapple T-Shirt" product for "$200" on the "Web-US" channel + And this customer has placed an order "#00000001" buying a single "Apple T-Shirt" product for "$100.00" on the "Web-US" channel + And this customer has also placed an order "#00000002" buying a single "Pineapple T-Shirt" product for "$200.00" on the "Web-US" channel And this customer has also placed an order "#00000003" buying a single "Pen T-Shirt" product for "$150.50" on the "Web-US" channel - And this customer has also placed an order "#00000004" buying a single "Apple T-Shirt" product for "£200" on the "Web-UK" channel + And this customer has also placed an order "#00000004" buying a single "Apple T-Shirt" product for "£200.00" on the "Web-UK" channel And this customer has also placed an order "#00000005" buying a single "Pineapple T-Shirt" product for "£150.50" on the "Web-UK" channel - And this customer has also placed an order "#00000006" buying a single "Pen T-Shirt" product for "£100" on the "Web-UK" channel + And this customer has also placed an order "#00000006" buying a single "Pen T-Shirt" product for "£100.00" on the "Web-UK" channel And I am logged in as an administrator And I am browsing orders - @ui + @ui @api Scenario: Filtering orders by currency alone When I choose "British Pound" as the filter currency And I filter Then I should see 3 orders in the list But I should not see any orders with currency "USD" - @ui + @ui @api Scenario: Filtering orders with total greater than specified amount When I choose "US Dollar" as the filter currency And I specify filter total being greater than 100 @@ -36,7 +36,7 @@ Feature: Filtering orders by total in different currencies But I should not see an order with "#00000001" number And I should not see any orders with currency "GBP" - @ui + @ui @api Scenario: Filtering orders with total less than specified amount When I choose "US Dollar" as the filter currency And I specify filter total being less than 200 @@ -47,7 +47,7 @@ Feature: Filtering orders by total in different currencies But I should not see an order with "#00000002" number And I should not see any orders with currency "GBP" - @ui + @ui @api Scenario: Filtering order with total from a specified range When I choose "British Pound" as the filter currency And I specify filter total being greater than 150.50 @@ -59,7 +59,7 @@ Feature: Filtering orders by total in different currencies And I should not see an order with "#00000006" number And I should not see any orders with currency "USD" - @ui + @ui @api Scenario: Filtering orders by total in given range but with no currency provided When I specify filter total being greater than 150 And I specify filter total being less than 200 diff --git a/features/order/managing_orders/order_filtration/filtering_orders_by_variants.feature b/features/order/managing_orders/order_filtration/filtering_orders_by_variants.feature index 718193b62b..2022ef8cd1 100644 --- a/features/order/managing_orders/order_filtration/filtering_orders_by_variants.feature +++ b/features/order/managing_orders/order_filtration/filtering_orders_by_variants.feature @@ -9,10 +9,10 @@ Feature: Filtering orders by variants And the store ships everywhere for Free And the store allows paying Offline And the store has a product "Galaxy Shirt" with code "cosmic-tee" - And this product has "Nebula Top" variant priced at "$25" - And this product also has "Neutron Sleeveless" variant priced at "$20" + And this product has "Nebula Top" variant priced at "$25.00" + And this product also has "Neutron Sleeveless" variant priced at "$20.00" And the store has a product "Space Dress" with code "cosmic-dress" - And this product has "Sundress" variant priced at "$40" + And this product has "Sundress" variant priced at "$40.00" And there is a customer "tanith@low.com" that placed an order "#0000001" And the customer bought a single "Nebula Top" variant of product "Galaxy Shirt" And the customer also bought a "Neutron Sleeveless" variant of product "Galaxy Shirt" @@ -26,7 +26,7 @@ Feature: Filtering orders by variants And the customer chose "Free" shipping method to "United States" with "Offline" payment And I am logged in as an administrator - @ui @javascript + @ui @api @javascript Scenario: Filtering orders by variant When I browse orders And I filter by variant "Sundress" @@ -34,7 +34,7 @@ Feature: Filtering orders by variants And I should see an order with "#0000002" number And I should see an order with "#0000003" number - @ui @javascript + @ui @api @javascript Scenario: Filtering orders by multiple variants of the same product When I browse orders And I filter by variants "Nebula Top" and "Neutron Sleeveless" @@ -42,7 +42,7 @@ Feature: Filtering orders by variants And I should see an order with "#0000001" number And I should see an order with "#0000002" number - @ui @javascript + @ui @api @javascript Scenario: Filtering orders by multiple variants of different products When I browse orders And I filter by variants "Neutron Sleeveless" and "Sundress" diff --git a/features/order/managing_orders/order_history/tracking_changes_on_order_addresses.feature b/features/order/managing_orders/order_history/tracking_changes_on_order_addresses.feature index 5fa276830c..c1fe15488a 100644 --- a/features/order/managing_orders/order_history/tracking_changes_on_order_addresses.feature +++ b/features/order/managing_orders/order_history/tracking_changes_on_order_addresses.feature @@ -8,25 +8,27 @@ Feature: Tracking changes on order addresses Given the store operates on a single channel in "United States" And the store ships everywhere for Free And the store allows paying with "Cash on Delivery" - And the store has a product "Italian suit" priced at "$4000.00" + And the store has a product "Italian suit" priced at "$4,000.00" And there is a customer "barney@stinson.com" that placed an order "#00000001" And the customer bought a single "Italian suit" When I am logged in as an administrator - @ui + @api @ui Scenario: Browsing order's addresses history after changing it by customer Given the customer "Barney Stinson" addressed it to "East 84st Street and 3rd Avenue", "10118" "New York" in the "United States" with identical billing address And the customer changed shipping address' street to "211 Madison Avenue" And the customer chose "Free" shipping method with "Cash on Delivery" payment When I browse order's "#00000001" history - Then there should be 2 changes in the registry + Then there should be 2 shipping address changes in the registry + Then there should be 1 billing address changes in the registry - @ui + @api @ui Scenario: Browsing order's addresses history after changing it by administrator Given the customer "Barney Stinson" addressed it to "East 84st Street and 3rd Avenue", "10118" "New York" in the "United States" with identical billing address And the customer chose "Free" shipping method with "Cash on Delivery" payment - When I want to modify a customer's shipping address of this order - And I specify their shipping address as "New York", "150 W. 85th Street", "10028", "United States" for "Ted Mosby" + When I want to modify a customer's billing address of this order + And I specify their billing address as "New York", "150 W. 85th Street", "10028", "United States" for "Ted Mosby" And I save my changes And I browse order's "#00000001" history - Then there should be 2 changes in the registry + Then there should be 1 shipping address changes in the registry + Then there should be 2 billing address changes in the registry diff --git a/features/order/managing_orders/resending_order_confirmation_email.feature b/features/order/managing_orders/resending_order_confirmation_email.feature index 89ec59f179..4be1e5bc49 100644 --- a/features/order/managing_orders/resending_order_confirmation_email.feature +++ b/features/order/managing_orders/resending_order_confirmation_email.feature @@ -16,17 +16,24 @@ Feature: Resending an order confirmation email for a chosen order And the customer chose "Free" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui @email - Scenario: Resending a confirmation email for a given order + @ui @email @api + Scenario: Resending a confirmation email for an order When I view the summary of the order "#00000666" And I resend the order confirmation email Then I should be notified that the order confirmation email has been successfully resent to the customer And an email with the confirmation of the order "#00000666" should be sent to "lucy@teamlucifer.com" - @ui @email - Scenario: Sending a confirmation email after shipping an order in different locale than the default one + @ui @email @api + Scenario: Resending a confirmation email for an order in different locale than the default one Given the order "#00000666" has been placed in "Polish (Poland)" locale When I view the summary of the order "#00000666" And I resend the order confirmation email Then I should be notified that the order confirmation email has been successfully resent to the customer And an email with the confirmation of the order "#00000666" should be sent to "lucy@teamlucifer.com" in "Polish (Poland)" locale + + @ui @email @api + Scenario: Not being able to resend a confirmation email for an order with wrong state + When I view the summary of the order "#00000666" + And I cancel this order + Then I should not be able to resend the order confirmation email + And an email with the confirmation of the order "#00000666" should not be sent to "lucy@teamlucifer.com" diff --git a/features/payment/managing_payment_methods/adding_payment_method.feature b/features/payment/managing_payment_methods/adding_payment_method.feature index 3928ab45c8..ce36a1cf0d 100644 --- a/features/payment/managing_payment_methods/adding_payment_method.feature +++ b/features/payment/managing_payment_methods/adding_payment_method.feature @@ -8,7 +8,7 @@ Feature: Adding a new payment method Given the store operates on a single channel in "United States" And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new payment method When I want to create a new Offline payment method And I name it "Offline" in "English (United States)" @@ -17,7 +17,7 @@ Feature: Adding a new payment method Then I should be notified that it has been successfully created And the payment method "Offline" should appear in the registry - @ui + @ui @api Scenario: Adding a new payment method with description When I want to create a new Offline payment method And I name it "Offline" in "English (United States)" @@ -27,7 +27,7 @@ Feature: Adding a new payment method Then I should be notified that it has been successfully created And the payment method "Offline" should appear in the registry - @ui + @ui @api Scenario: Adding a new payment method with instructions When I want to create a new Offline payment method And I name it "Offline" in "English (United States)" @@ -38,7 +38,7 @@ Feature: Adding a new payment method And the payment method "Offline" should appear in the registry And the payment method "Offline" should have instructions "Bank account: 0000 1111 2222 3333" in "English (United States)" - @ui + @ui @api Scenario: Adding a new payment method for channel When I want to create a new Offline payment method And I name it "Offline" in "English (United States)" @@ -49,7 +49,7 @@ Feature: Adding a new payment method And the payment method "Offline" should appear in the registry And the payment method "Offline" should be available in channel "United States" - @ui + @ui @api Scenario: Adding a new paypal payment method When I want to create a new payment method with "Paypal Express Checkout" gateway factory And I name it "Paypal Express Checkout" in "English (United States)" @@ -59,7 +59,7 @@ Feature: Adding a new payment method Then I should be notified that it has been successfully created And the payment method "Paypal Express Checkout" should appear in the registry - @ui + @ui @api Scenario: Adding a new stripe payment method When I want to create a new payment method with "Stripe Checkout" gateway factory And I name it "Stripe Checkout" in "English (United States)" diff --git a/features/payment/managing_payment_methods/browsing_payment_methods.feature b/features/payment/managing_payment_methods/browsing_payment_methods.feature index 0eca4c2b18..37288549b4 100644 --- a/features/payment/managing_payment_methods/browsing_payment_methods.feature +++ b/features/payment/managing_payment_methods/browsing_payment_methods.feature @@ -7,7 +7,7 @@ Feature: Browsing payment methods Background: Given I am logged in as an administrator - @ui + @ui @api Scenario: Browsing defined payment methods Given the store has a payment method "Offline" with a code "OFF" And the store has a payment method "PayPal Express Checkout" with a code "PAYPAL" and Paypal Express Checkout gateway diff --git a/features/payment/managing_payment_methods/deleting_multiple_payment_methods.feature b/features/payment/managing_payment_methods/deleting_multiple_payment_methods.feature index 26cf6315a0..1dd7d0c9de 100644 --- a/features/payment/managing_payment_methods/deleting_multiple_payment_methods.feature +++ b/features/payment/managing_payment_methods/deleting_multiple_payment_methods.feature @@ -10,7 +10,7 @@ Feature: Deleting multiple payment methods And the store has also a payment method "PayPal Express Checkout" with a code "paypal" and Paypal Express Checkout gateway And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple payment methods at once When I browse payment methods And I check the "Offline" payment method diff --git a/features/payment/managing_payment_methods/deleting_payment_method.feature b/features/payment/managing_payment_methods/deleting_payment_method.feature index 06152a4bb4..3aafd8fe79 100644 --- a/features/payment/managing_payment_methods/deleting_payment_method.feature +++ b/features/payment/managing_payment_methods/deleting_payment_method.feature @@ -8,7 +8,7 @@ Feature: Deleting payment methods Given the store has a payment method "Offline" with a code "Offline" And I am logged in as an administrator - @ui + @ui @api Scenario: Deleted payment method should disappear from the registry When I delete the "Offline" payment method Then I should be notified that it has been successfully deleted diff --git a/features/payment/managing_payment_methods/editing_payment_method.feature b/features/payment/managing_payment_methods/editing_payment_method.feature index 5d0b52ed13..d3ca12fa2d 100644 --- a/features/payment/managing_payment_methods/editing_payment_method.feature +++ b/features/payment/managing_payment_methods/editing_payment_method.feature @@ -9,7 +9,7 @@ Feature: Editing payment methods And the store has a payment method "Offline" with a code "Offline" And I am logged in as an administrator - @ui + @ui @api Scenario: Renaming the payment method When I want to modify the "Offline" payment method And I rename it to "Cash on delivery" in "English (United States)" @@ -17,7 +17,7 @@ Feature: Editing payment methods Then I should be notified that it has been successfully edited And this payment method name should be "Cash on delivery" - @ui + @ui @api Scenario: Disabling payment method When I want to modify the "Offline" payment method And I disable it @@ -25,7 +25,7 @@ Feature: Editing payment methods Then I should be notified that it has been successfully edited And this payment method should be disabled - @ui + @ui @api Scenario: Enabling payment method Given the payment method "Offline" is disabled When I want to modify the "Offline" payment method @@ -34,12 +34,12 @@ Feature: Editing payment methods Then I should be notified that it has been successfully edited And this payment method should be enabled - @ui - Scenario: Seeing disabled code field while editing payment method + @ui @api + Scenario: Being unable to edit code of an existing payment method When I want to modify the "Offline" payment method - Then the code field should be disabled + Then I should not be able to edit its code - @ui - Scenario: Seeing disabled gateway factory field while editing payment method + @ui @api + Scenario: Being unable to edit gateway factory field of existing payment method When I want to modify the "Offline" payment method Then the factory name field should be disabled diff --git a/features/payment/managing_payment_methods/payment_method_unique_code_validation.feature b/features/payment/managing_payment_methods/payment_method_unique_code_validation.feature index aba52d79e3..d448fe933f 100644 --- a/features/payment/managing_payment_methods/payment_method_unique_code_validation.feature +++ b/features/payment/managing_payment_methods/payment_method_unique_code_validation.feature @@ -9,7 +9,7 @@ Feature: Payment method unique code validation And the store has a payment method "Offline" with a code "Offline" And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add payment method with taken code When I want to create a new Offline payment method And I name it "Paypal Express Checkout" in "English (United States)" diff --git a/features/payment/managing_payment_methods/payment_method_validation.feature b/features/payment/managing_payment_methods/payment_method_validation.feature index c62b153714..6ef6d19854 100644 --- a/features/payment/managing_payment_methods/payment_method_validation.feature +++ b/features/payment/managing_payment_methods/payment_method_validation.feature @@ -9,7 +9,7 @@ Feature: Payment method validation And the store has a payment method "Offline" with a code "Offline" And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add a new payment method without specifying its code When I want to create a new payment method with "Paypal Express Checkout" gateway factory And I name it "Paypal Express Checkout" in "English (United States)" @@ -18,17 +18,17 @@ Feature: Payment method validation Then I should be notified that code is required And the payment method with name "Paypal Express Checkout" should not be added - @ui + @ui @api Scenario: Trying to add a new payment method without specifying its name When I want to create a new payment method with "Paypal Express Checkout" gateway factory And I specify its code as "PEC" But I do not name it And I add it - Then I should be notified that name is required + Then I should be notified that I have to specify payment method name And the payment method with code "PEC" should not be added - @ui - Scenario: Trying to add a new paypal payment method without specifying required configuration + @ui @api + Scenario: Trying to add a new paypal payment method without specifying password When I want to create a new payment method with "Paypal Express Checkout" gateway factory And I name it "Paypal Express Checkout" in "English (United States)" And I specify its code as "PEC" @@ -38,7 +38,79 @@ Feature: Payment method validation Then I should be notified that I have to specify paypal password And the payment method with code "PEC" should not be added - @ui + @no-ui @api + Scenario: Trying to add a new paypal payment method without specifying sandbox + When I want to create a new payment method with "Paypal Express Checkout" gateway factory + And I name it "Paypal Express Checkout" in "English (United States)" + And I specify its code as "PEC" + And I configure it for username "TEST" with "TEST" signature and password, but without sandbox + And I add it + Then I should be notified that I have to specify paypal sandbox status + And the payment method with code "PEC" should not be added + + @no-ui @api + Scenario: Trying to add a new paypal payment method, but with sandbox that has wrong type + When I want to create a new payment method with "Paypal Express Checkout" gateway factory + And I name it "Paypal Express Checkout" in "English (United States)" + And I specify its code as "PEC" + And I configure it for username "TEST" with "TEST" signature and password, but with sandbox that has wrong type + And I add it + Then I should be notified that I have to specify paypal sandbox status that is boolean + And the payment method with code "PEC" should not be added + + @ui @api + Scenario: Trying to add a new stripe payment method with only publishable key specified + When I want to create a new payment method with "Stripe Checkout" gateway factory + And I name it "Stripe Checkout" in "English (United States)" + And I specify its code as "SC" + And I configure it with only "publishable key" + And I add it + Then I should be notified that I have to specify stripe "secret key" + And the payment method with code "PEC" should not be added + + @ui @api + Scenario: Trying to add a new stripe payment method with only secret key specified + When I want to create a new payment method with "Stripe Checkout" gateway factory + And I name it "Stripe Checkout" in "English (United States)" + And I specify its code as "SC" + And I configure it with only "secret key" + And I add it + Then I should be notified that I have to specify stripe "publishable key" + And the payment method with code "PEC" should not be added + + @no-ui @api + Scenario: Trying to add a new payment method without gateway configuration + When I want to create a new payment method without gateway configuration + And I name it "Payment method without gateway configuration" in "English (United States)" + And I specify its code as "PMWGC" + And I try to add it + Then I should be notified that I have to specify gateway configuration + + @no-ui @api + Scenario: Trying to add a new payment method without gateway name + When I want to create a new payment method without gateway name + And I name it "Payment method without gateway name" in "English (United States)" + And I specify its code as "PMWGN" + And I try to add it + Then I should be notified that I have to specify gateway name + + @no-ui @api + Scenario: Trying to add a new payment method without factory name + When I want to create a new payment method without factory name + And I name it "Payment method without gateway factory name" in "English (United States)" + And I specify its code as "PMWFN" + And I try to add it + Then I should be notified that I have to specify factory name + + @no-ui @api + Scenario: Trying to add a new payment method with wrong factory name + When I want to create a new payment method with wrong factory name + And I name it "Payment method with wrong gateway factory name" in "English (United States)" + And I specify its code as "PMWWFN" + And I try to add it + Then I should be notified that I have to specify factory name that is available + + @ui @api Scenario: Trying to remove name from an existing payment method When I want to modify the "Offline" payment method And I remove its name from "English (United States)" translation diff --git a/features/payment/managing_payment_methods/preventing_deletion_of_used_payment_method.feature b/features/payment/managing_payment_methods/preventing_deletion_of_used_payment_method.feature index 2c02f62455..f847f46a28 100644 --- a/features/payment/managing_payment_methods/preventing_deletion_of_used_payment_method.feature +++ b/features/payment/managing_payment_methods/preventing_deletion_of_used_payment_method.feature @@ -14,7 +14,7 @@ Feature: Prevent deletion of used payment method And the customer chose "DHL Express" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @ui @api Scenario: Being unable to delete a payment method which is in use When I try to delete the "Cash on Delivery" payment method Then I should be notified that it is in use diff --git a/features/payment/managing_payment_methods/sorting_payment_methods.feature b/features/payment/managing_payment_methods/sorting_payment_methods.feature index a8f5b294ad..b30c79cad3 100644 --- a/features/payment/managing_payment_methods/sorting_payment_methods.feature +++ b/features/payment/managing_payment_methods/sorting_payment_methods.feature @@ -15,37 +15,37 @@ Feature: Sorting listed payment methods And this payment method is named "Płatność Przy Odbiorze" in the "Polish (Poland)" locale And I am logged in as an administrator - @ui + @ui @api Scenario: Payment methods can be sorted by their codes Given I am browsing payment methods When I start sorting payment methods by code Then I should see 3 payment methods in the list And the first payment method on the list should have code "cash_on_delivery" - @ui + @ui @api Scenario: Changing the order of sorting payment methods by their codes Given I am browsing payment methods And the payment methods are already sorted by code - When I switch the way payment methods are sorted by code + When I switch the way payment methods are sorted to descending by code Then I should see 3 payment methods in the list And the first payment method on the list should have code "PAYPAL" - @ui + @ui @api Scenario: Payment methods can be sorted by their names Given I am browsing payment methods When I start sorting payment methods by name Then I should see 3 payment methods in the list And the first payment method on the list should have name "Cash on Delivery" - @ui + @ui @api Scenario: Changing the order of sorting payment methods by their names Given I am browsing payment methods And the payment methods are already sorted by name - When I switch the way payment methods are sorted by name + When I switch the way payment methods are sorted to descending by name Then I should see 3 payment methods in the list And the first payment method on the list should have name "PayPal Express Checkout" - @ui + @ui @api Scenario: Payment methods are always sorted in the default locale Given I change my locale to "Polish (Poland)" And I am browsing payment methods diff --git a/features/payment/managing_payment_methods/sorting_payment_methods_by_position.feature b/features/payment/managing_payment_methods/sorting_payment_methods_by_position.feature index 1c79719210..f4778dfe95 100644 --- a/features/payment/managing_payment_methods/sorting_payment_methods_by_position.feature +++ b/features/payment/managing_payment_methods/sorting_payment_methods_by_position.feature @@ -11,20 +11,20 @@ Feature: Sorting listed payment methods by position And the store allows paying with "Offline" at position 1 And I am logged in as an administrator - @ui + @ui @api Scenario: Payment methods are sorted by position in ascending order by default When I browse payment methods Then I should see 3 payment methods in the list And the first payment method on the list should have name "Paypal Express Checkout" And the last payment method on the list should have name "Cash on Delivery" - @ui + @ui @api Scenario: Payment method added at no position is added as the last one Given the store allows paying with "Credit Card" When I browse payment methods Then the last payment method on the list should have name "Credit Card" - @ui + @ui @api Scenario: Payment method added at position 0 is added as the first one Given the store also allows paying with "Credit Card" at position 0 When I browse payment methods diff --git a/features/payment/managing_payments/accessing_payments_order_from_payment_index.feature b/features/payment/managing_payments/accessing_payments_order_from_payment_index.feature index e05b984b32..4012c30634 100644 --- a/features/payment/managing_payments/accessing_payments_order_from_payment_index.feature +++ b/features/payment/managing_payments/accessing_payments_order_from_payment_index.feature @@ -12,8 +12,8 @@ Feature: Accessing payment's order from the payment index And there is an "#00000001" order with "Apple" product And I am logged in as an administrator - @ui - Scenario: Accessing payment's order from the payments index + @ui @api + Scenario: Accessing payment's order from the payment Given I am browsing payments When I go to the details of the first payment's order - Then I should see order page with details of order "#00000001" + Then I should see the details of order "#00000001" diff --git a/features/product/adding_product_reviews/adding_product_review_as_a_customer.feature b/features/product/adding_product_reviews/adding_product_review_as_a_customer.feature index 928da4b50d..2790d8c294 100644 --- a/features/product/adding_product_reviews/adding_product_review_as_a_customer.feature +++ b/features/product/adding_product_reviews/adding_product_review_as_a_customer.feature @@ -17,7 +17,7 @@ Feature: Adding product review as a customer And I add it Then I should be notified that my review is waiting for the acceptation - @ui + @ui @no-api Scenario: Adding product reviews as a logged in customer with remember me option Given I am a logged in customer by using remember me option When I want to review product "Necronomicon" diff --git a/features/product/adding_product_reviews/product_review_validation.feature b/features/product/adding_product_reviews/product_review_validation.feature index a9da7f172b..0c7070bf8f 100644 --- a/features/product/adding_product_reviews/product_review_validation.feature +++ b/features/product/adding_product_reviews/product_review_validation.feature @@ -72,4 +72,4 @@ Feature: Product review validation And I leave a comment "This book made me sad, but plot was fine.", titled "Not good, not bad" as "example@example.com" And I rate it with 6 points And I try to add it - Then I should be notified that rate must be an integer in the range 1-5 + Then I should be notified that rating must be between 1 and 5 diff --git a/features/product/managing_price_history/deleting_old_channel_pricing_entry_logs.feature b/features/product/managing_price_history/deleting_old_channel_pricing_entry_logs.feature new file mode 100644 index 0000000000..ad40222dd7 --- /dev/null +++ b/features/product/managing_price_history/deleting_old_channel_pricing_entry_logs.feature @@ -0,0 +1,31 @@ +@managing_price_history +Feature: Deleting old channel pricing entry logs + In order to keep the price history slim + As an Administrator + I want to delete old channel pricing entry logs + + Background: + Given the store operates on a single channel in "United States" + And it is "2022-03-14" now + And the store has a product "PHP T-Shirt" priced at "$10.00" + And on "2022-03-20" its price changed to "$20.00" + And on "2022-03-29" its original price changed to "$25.00" + And on "2022-04-14" its price changed to "$15.00" and original price to "$30.00" + And on "2022-04-20" its original price has been removed + And it is "2022-04-29" now + + @domain + Scenario: Deleting price history older than 90 days + When I delete price history older than 90 days + Then there should be 5 price history entries for this product + + @domain + Scenario: Deleting price history older than 30 days + When I delete price history older than 30 days + Then there should be 2 price history entries for this product + And this product should have no entry with original price changed to "$25.00" + + @domain + Scenario: Deleting price history older than 1 day + When I delete price history older than 1 day + Then this product's price history should be empty diff --git a/features/product/managing_product_association_types/deleting_multiple_product_association_types.feature b/features/product/managing_product_association_types/deleting_multiple_product_association_types.feature index f2e9d85140..52e7d75c54 100644 --- a/features/product/managing_product_association_types/deleting_multiple_product_association_types.feature +++ b/features/product/managing_product_association_types/deleting_multiple_product_association_types.feature @@ -10,7 +10,7 @@ Feature: Deleting multiple product association types And the store has also a product association type "Accessories" And I am logged in as an administrator - @ui @api @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple product association types at once When I browse product association types And I check the "Cross sell" product association type diff --git a/features/product/managing_product_attributes/adding_checkbox_product_attribute.feature b/features/product/managing_product_attributes/adding_checkbox_product_attribute.feature index 4a9a705abc..effe7330a6 100644 --- a/features/product/managing_product_attributes/adding_checkbox_product_attribute.feature +++ b/features/product/managing_product_attributes/adding_checkbox_product_attribute.feature @@ -8,7 +8,7 @@ Feature: Adding a new checkbox product attribute Given the store is available in "English (United States)" And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new checkbox product attribute When I want to create a new checkbox product attribute And I specify its code as "t_shirt_with_cotton" @@ -17,7 +17,7 @@ Feature: Adding a new checkbox product attribute Then I should be notified that it has been successfully created And the checkbox attribute "T-Shirt with cotton" should appear in the store - @ui + @ui @no-api Scenario: Seeing disabled type field while adding a checkbox product attribute When I want to create a new checkbox product attribute Then the type field should be disabled diff --git a/features/product/managing_product_attributes/adding_float_product_attribute.feature b/features/product/managing_product_attributes/adding_float_product_attribute.feature new file mode 100644 index 0000000000..570cfaa36a --- /dev/null +++ b/features/product/managing_product_attributes/adding_float_product_attribute.feature @@ -0,0 +1,23 @@ +@managing_product_attributes +Feature: Adding a new float product attribute + In order to show specific product's parameters to customer + As an Administrator + I want to add a new float product attribute + + Background: + Given the store is available in "English (United States)" + And I am logged in as an administrator + + @ui @api + Scenario: Adding a new float product attribute + When I want to create a new float product attribute + And I specify its code as "display_size" + And I name it "Display Size" in "English (United States)" + And I add it + Then I should be notified that it has been successfully created + And the float attribute "Display Size" should appear in the store + + @ui @no-api + Scenario: Seeing disabled type field while adding a float product attribute + When I want to create a new float product attribute + Then the type field should be disabled diff --git a/features/product/managing_product_attributes/adding_integer_product_attribute.feature b/features/product/managing_product_attributes/adding_integer_product_attribute.feature index 6b3bf3aadb..8a51d121d1 100644 --- a/features/product/managing_product_attributes/adding_integer_product_attribute.feature +++ b/features/product/managing_product_attributes/adding_integer_product_attribute.feature @@ -8,7 +8,7 @@ Feature: Adding a new integer product attribute Given the store is available in "English (United States)" And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new integer product attribute When I want to create a new integer product attribute And I specify its code as "book_pages" @@ -17,7 +17,7 @@ Feature: Adding a new integer product attribute Then I should be notified that it has been successfully created And the integer attribute "Book pages" should appear in the store - @ui + @ui @no-api Scenario: Seeing disabled type field while adding a integer product attribute When I want to create a new integer product attribute Then the type field should be disabled diff --git a/features/product/managing_product_attributes/adding_non_translatable_attribute.feature b/features/product/managing_product_attributes/adding_non_translatable_attribute.feature index ae5a4b7087..897e8fe7eb 100644 --- a/features/product/managing_product_attributes/adding_non_translatable_attribute.feature +++ b/features/product/managing_product_attributes/adding_non_translatable_attribute.feature @@ -8,7 +8,7 @@ Feature: Adding a new non-translatable product attribute Given the store is available in "English (United States)" And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new non-translatable product attribute When I want to create a new integer product attribute And I specify its code as "damage" diff --git a/features/product/managing_product_attributes/adding_percent_product_attribute.feature b/features/product/managing_product_attributes/adding_percent_product_attribute.feature index 7a8f9dd34d..8b4a7b8766 100644 --- a/features/product/managing_product_attributes/adding_percent_product_attribute.feature +++ b/features/product/managing_product_attributes/adding_percent_product_attribute.feature @@ -8,7 +8,7 @@ Feature: Adding a new percent product attribute Given the store is available in "English (United States)" And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new percent product attribute When I want to create a new percent product attribute And I specify its code as "t_shirt_cotton_content" @@ -17,7 +17,7 @@ Feature: Adding a new percent product attribute Then I should be notified that it has been successfully created And the percent attribute "T-Shirt cotton content" should appear in the store - @ui + @ui @no-api Scenario: Seeing disabled type field while adding a percent product attribute When I want to create a new percent product attribute Then the type field should be disabled diff --git a/features/product/managing_product_attributes/adding_select_product_attribute.feature b/features/product/managing_product_attributes/adding_select_product_attribute.feature index caecf44763..d50fb2471a 100644 --- a/features/product/managing_product_attributes/adding_select_product_attribute.feature +++ b/features/product/managing_product_attributes/adding_select_product_attribute.feature @@ -8,7 +8,7 @@ Feature: Adding a new select product attribute Given the store is available in "English (United States)" And I am logged in as an administrator - @ui @javascript + @ui @javascript @api Scenario: Adding a new select product attribute When I want to create a new select product attribute And I specify its code as "mug_material" @@ -18,7 +18,7 @@ Feature: Adding a new select product attribute Then I should be notified that it has been successfully created And the select attribute "Mug material" should appear in the store - @ui @javascript + @ui @javascript @api Scenario: Adding multiple select product attribute When I want to create a new select product attribute And I specify its code as "mug_material" @@ -30,7 +30,7 @@ Feature: Adding a new select product attribute Then I should be notified that it has been successfully created And the select attribute "Mug material" should appear in the store - @ui + @ui @no-api Scenario: Seeing disabled type field while adding a select product attribute When I want to create a new select product attribute Then the type field should be disabled diff --git a/features/product/managing_product_attributes/adding_text_product_attribute.feature b/features/product/managing_product_attributes/adding_text_product_attribute.feature index 92be2bf72b..41852229db 100644 --- a/features/product/managing_product_attributes/adding_text_product_attribute.feature +++ b/features/product/managing_product_attributes/adding_text_product_attribute.feature @@ -8,7 +8,7 @@ Feature: Adding a new text product attribute Given the store is available in "English (United States)" And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new text product attribute When I want to create a new text product attribute And I specify its code as "t_shirt_brand" @@ -17,7 +17,7 @@ Feature: Adding a new text product attribute Then I should be notified that it has been successfully created And the text attribute "T-Shirt brand" should appear in the store - @ui + @ui @no-api Scenario: Seeing disabled type field while adding text a product attribute When I want to create a new text product attribute Then the type field should be disabled diff --git a/features/product/managing_product_attributes/adding_textarea_product_attribute.feature b/features/product/managing_product_attributes/adding_textarea_product_attribute.feature index d5fc758ade..b8934594bf 100644 --- a/features/product/managing_product_attributes/adding_textarea_product_attribute.feature +++ b/features/product/managing_product_attributes/adding_textarea_product_attribute.feature @@ -8,7 +8,7 @@ Feature: Adding a new textarea product attribute Given the store is available in "English (United States)" And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new textarea product attribute When I want to create a new textarea product attribute And I specify its code as "t_shirt_details" @@ -17,7 +17,7 @@ Feature: Adding a new textarea product attribute Then I should be notified that it has been successfully created And the textarea attribute "T-Shirt details" should appear in the store - @ui + @ui @no-api Scenario: Seeing disabled type field while adding a textarea product attribute When I want to create a new textarea product attribute Then the type field should be disabled diff --git a/features/product/managing_product_attributes/browsing_all_product_attributes.feature b/features/product/managing_product_attributes/browsing_all_product_attributes.feature index 1c9740ea17..0d30a90736 100644 --- a/features/product/managing_product_attributes/browsing_all_product_attributes.feature +++ b/features/product/managing_product_attributes/browsing_all_product_attributes.feature @@ -10,7 +10,7 @@ Feature: Browsing product attributes And the store has a integer product attribute "Book pages" with code "book_pages" And I am logged in as an administrator - @ui + @ui @api Scenario: Browsing all product attributes in store When I want to see all product attributes in store Then I should see 3 product attributes in the list diff --git a/features/product/managing_product_attributes/deleting_multiple_product_attributes.feature b/features/product/managing_product_attributes/deleting_multiple_product_attributes.feature index a2c1bb0319..43137fc4a3 100644 --- a/features/product/managing_product_attributes/deleting_multiple_product_attributes.feature +++ b/features/product/managing_product_attributes/deleting_multiple_product_attributes.feature @@ -10,7 +10,7 @@ Feature: Deleting multiple product attributes And the store has a integer product attribute "Pages" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple product attributes at once When I browse product attributes And I check the "Publisher" product attribute diff --git a/features/product/managing_product_attributes/remove_product_attribute.feature b/features/product/managing_product_attributes/deleting_product_attribute.feature similarity index 72% rename from features/product/managing_product_attributes/remove_product_attribute.feature rename to features/product/managing_product_attributes/deleting_product_attribute.feature index 3a59963f12..a099670a31 100644 --- a/features/product/managing_product_attributes/remove_product_attribute.feature +++ b/features/product/managing_product_attributes/deleting_product_attribute.feature @@ -1,5 +1,5 @@ @managing_product_attributes -Feature:Removing a attribute +Feature: Deleting a product attribute In order to keep my collection of product attributes not cluttered As an administrator I want to be able to remove an attribute that is not assigned to any of the products @@ -8,14 +8,14 @@ Feature:Removing a attribute Given I am logged in as an administrator And the store has a product "44 Magnum" - @ui - Scenario: Try deleting a attribute from the registry when product use him + @ui @api + Scenario: Trying to delete an attribute from the registry when a product uses it Given this product has a text attribute "Gun caliber" with value "11 mm" When I delete this product attribute - Then I should be notified that it has been failed deleted "product attribute" + Then I should be notified that it is in use - @ui - Scenario: Deleting a text product attribute when not by used + @ui @api + Scenario: Deleting a text product attribute when it's not used Given the store has a text product attribute "Gun caliber" When I delete this product attribute Then I should be notified that it has been successfully deleted diff --git a/features/product/managing_product_attributes/deleting_text_product_attribute.feature b/features/product/managing_product_attributes/deleting_text_product_attribute.feature deleted file mode 100644 index 562e7dd120..0000000000 --- a/features/product/managing_product_attributes/deleting_text_product_attribute.feature +++ /dev/null @@ -1,15 +0,0 @@ -@managing_product_attributes -Feature: Deleting a text product attribute - In order to remove test, obsolete or incorrect text product attribute - As an Administrator - I want to be able to delete a text product attribute - - Background: - Given I am logged in as an administrator - - @ui - Scenario: Deleting a text product attribute from the registry - Given the store has a text product attribute "T-Shirt cotton brand" - When I delete this product attribute - Then I should be notified that it has been successfully deleted - And this product attribute should no longer exist in the registry diff --git a/features/product/managing_product_attributes/editing_select_product_attribute.feature b/features/product/managing_product_attributes/editing_select_product_attribute.feature index d340f2a532..f896089fae 100644 --- a/features/product/managing_product_attributes/editing_select_product_attribute.feature +++ b/features/product/managing_product_attributes/editing_select_product_attribute.feature @@ -9,7 +9,7 @@ Feature: Editing a select product attribute And the store has a select product attribute "T-Shirt material" with value "Banana skin" And I am logged in as an administrator - @ui + @ui @api Scenario: Editing a select product attribute name When I want to edit this product attribute And I change its name to "T-Shirt material" in "English (United States)" @@ -17,7 +17,7 @@ Feature: Editing a select product attribute Then I should be notified that it has been successfully edited And the select attribute "T-Shirt material" should appear in the store - @ui + @ui @api Scenario: Editing a select product attribute value When I want to edit this product attribute And I change its value "Banana skin" to "Orange skin" @@ -25,7 +25,7 @@ Feature: Editing a select product attribute Then I should be notified that it has been successfully edited And this product attribute should have value "Orange skin" - @ui @javascript + @ui @javascript @api Scenario: Adding a new value to an existing select product attribute When I want to edit this product attribute And I add value "Orange skin" in "English (United States)" @@ -33,7 +33,7 @@ Feature: Editing a select product attribute Then I should be notified that it has been successfully edited And this product attribute should have value "Orange skin" - @ui @javascript @no-postgres # LIKE on a json column issue + @ui @javascript @api @no-postgres # LIKE on a json column issue Scenario: Deleting a value from an existing select product attribute When I want to edit this product attribute And I delete value "Banana skin" @@ -41,12 +41,12 @@ Feature: Editing a select product attribute Then I should be notified that it has been successfully edited And this product attribute should not have value "Banana skin" - @ui - Scenario: Seeing disabled code field while editing a product attribute + @ui @api + Scenario: Being unable to change code of an existing product attribute When I want to edit this product attribute - Then the code field should be disabled + Then I should not be able to edit its code - @ui - Scenario: Seeing disabled type field while editing a product attribute + @ui @api + Scenario: Being unable to change type of an existing product attribute When I want to edit this product attribute - Then the type field should be disabled + Then I should not be able to edit its type diff --git a/features/product/managing_product_attributes/editing_text_product_attribute.feature b/features/product/managing_product_attributes/editing_text_product_attribute.feature index f1237de6bb..94403887b7 100644 --- a/features/product/managing_product_attributes/editing_text_product_attribute.feature +++ b/features/product/managing_product_attributes/editing_text_product_attribute.feature @@ -6,25 +6,23 @@ Feature: Editing a text product attribute Background: Given the store is available in "English (United States)" + And the store has a text product attribute "T-Shirt cotton brand" with code "t_shirt_brand" And I am logged in as an administrator - @ui + @ui @api Scenario: Editing product attribute name - Given the store has a text product attribute "T-Shirt cotton brand" When I want to edit this product attribute And I change its name to "T-Shirt material" in "English (United States)" And I save my changes Then I should be notified that it has been successfully edited And the text attribute "T-Shirt material" should appear in the store - @ui - Scenario: Seeing disabled code field while editing a product attribute - Given the store has a text product attribute "T-Shirt cotton brand" with code "t_shirt_brand" + @ui @api + Scenario: Being unable to change code of an existing product attribute When I want to edit this product attribute - Then the code field should be disabled + Then I should not be able to edit its code - @ui - Scenario: Seeing disabled type field while editing a product attribute - Given the store has a text product attribute "T-Shirt cotton brand" with code "t_shirt_brand" + @ui @api + Scenario: Being unable to change type of an existing product attribute When I want to edit this product attribute - Then the type field should be disabled + Then I should not be able to edit its type diff --git a/features/product/managing_product_attributes/product_attribute_unique_code_validation.feature b/features/product/managing_product_attributes/product_attribute_unique_code_validation.feature index 96647ab472..dab34888e7 100644 --- a/features/product/managing_product_attributes/product_attribute_unique_code_validation.feature +++ b/features/product/managing_product_attributes/product_attribute_unique_code_validation.feature @@ -8,7 +8,7 @@ Feature: Product attribute unique code validation Given the store is available in "English (United States)" And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add a new product attribute with taken code Given the store has a text product attribute "T-Shirt cotton material" with code "t_shirt_material" When I want to create a new text product attribute diff --git a/features/product/managing_product_attributes/seeing_correct_select_attribute_values_in_different_locale_than_default_one.feature b/features/product/managing_product_attributes/seeing_correct_select_attribute_values_in_different_locale_than_default_one.feature index bbf8046ca8..9362431da5 100644 --- a/features/product/managing_product_attributes/seeing_correct_select_attribute_values_in_different_locale_than_default_one.feature +++ b/features/product/managing_product_attributes/seeing_correct_select_attribute_values_in_different_locale_than_default_one.feature @@ -8,7 +8,7 @@ Feature: Seeing correct select attribute values in different locale than default Given the store is available in "French (France)" And I am logged in as an administrator - @ui @javascript + @ui @javascript @no-api Scenario: Seeing correct attribute values in different locale than default one When I want to create a new select product attribute And I specify its code as "mug_material" diff --git a/features/product/managing_product_attributes/select_product_attribute_validation.feature b/features/product/managing_product_attributes/select_product_attribute_validation.feature index fd2775a52d..eed542b5eb 100644 --- a/features/product/managing_product_attributes/select_product_attribute_validation.feature +++ b/features/product/managing_product_attributes/select_product_attribute_validation.feature @@ -8,7 +8,7 @@ Feature: Select product attribute validation Given the store is available in "English (United States)" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Trying to add a new select product attribute with wrong max entries value When I want to create a new select product attribute And I name it "Mug material" in "English (United States)" @@ -22,7 +22,7 @@ Feature: Select product attribute validation Then I should be notified that max entries value must be greater or equal to the min entries value And the attribute with code "mug_material" should not appear in the store - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Trying to add a new select product attribute with wrong min entries value When I want to create a new select product attribute And I name it "Mug material" in "English (United States)" @@ -36,7 +36,7 @@ Feature: Select product attribute validation Then I should be notified that min entries value must be lower or equal to the number of added choices And the attribute with code "mug_material" should not appear in the store - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Trying to add a new select product attribute with specified entries values but without multiple option When I want to create a new select product attribute And I name it "Mug material" in "English (United States)" diff --git a/features/product/managing_product_attributes/sorting_product_attributes_by_position.feature b/features/product/managing_product_attributes/sorting_product_attributes_by_position.feature index c6d8a427de..e4a77d1bb6 100644 --- a/features/product/managing_product_attributes/sorting_product_attributes_by_position.feature +++ b/features/product/managing_product_attributes/sorting_product_attributes_by_position.feature @@ -10,21 +10,21 @@ Feature: Sorting listed product attributes by position And the store has a integer product attribute "Book pages" at position 0 And I am logged in as an administrator - @ui + @ui @api Scenario: Product attributes are sorted by position in ascending order by default When I want to see all product attributes in store Then I should see 3 product attributes in the list And the first product attribute on the list should have name "Book pages" And the last product attribute on the list should have name "T-Shirt with cotton" - @ui + @ui @api Scenario: Product attribute added at no position gets put at the bottom of the list Given the store has also a text product attribute "Drive type" When I want to see all product attributes in store Then I should see 4 product attributes in the list And the last product attribute on the list should have name "Drive type" - @ui + @ui @api Scenario: Product attribute added at position 0 is added as the first one Given the store has also a text product attribute "Drive type" at position 0 When I want to see all product attributes in store diff --git a/features/product/managing_product_attributes/text_product_attribute_validation.feature b/features/product/managing_product_attributes/text_product_attribute_validation.feature index 8f683e2297..f12c9ed525 100644 --- a/features/product/managing_product_attributes/text_product_attribute_validation.feature +++ b/features/product/managing_product_attributes/text_product_attribute_validation.feature @@ -8,7 +8,7 @@ Feature: Text product attribute validation Given the store is available in "English (United States)" And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add a new text product attribute without name When I want to create a new text product attribute And I specify its code as "t_shirt_with_cotton" @@ -17,7 +17,7 @@ Feature: Text product attribute validation Then I should be notified that name is required And the attribute with code "t_shirt_with_cotton" should not appear in the store - @ui + @ui @api Scenario: Trying to add a new text product attribute without code When I want to create a new text product attribute And I name it "T-Shirt brand" in "English (United States)" @@ -26,16 +26,16 @@ Feature: Text product attribute validation Then I should be notified that code is required And the attribute with name "T-Shirt brand" should not appear in the store - @ui + @ui @api Scenario: Trying to remove name for existing text product attribute Given the store has a text product attribute "T-Shirt cotton brand" When I want to edit this product attribute And I remove its name from "English (United States)" translation And I try to save my changes Then I should be notified that name is required - And the attribute with code "t_shirt_with_cotton" should not appear in the store + And the text attribute "T-Shirt cotton brand" should still be in the store - @ui + @ui @api Scenario: Trying to add a new text product attribute with wrong configuration When I want to create a new text product attribute And I name it "T-Shirt brand" in "English (United States)" diff --git a/features/product/managing_product_options/deleting_multiple_product_options.feature b/features/product/managing_product_options/deleting_multiple_product_options.feature index 54ef0f8fe6..269f1480f9 100644 --- a/features/product/managing_product_options/deleting_multiple_product_options.feature +++ b/features/product/managing_product_options/deleting_multiple_product_options.feature @@ -10,7 +10,7 @@ Feature: Deleting multiple product options And the store has also a product option "T-Shirt brand" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple product options at once When I browse product options And I check the "T-Shirt size" product option diff --git a/features/product/managing_product_options/managing_values_of_product_option.feature b/features/product/managing_product_options/managing_values_of_product_option.feature index b33f695874..330fbc7b2c 100644 --- a/features/product/managing_product_options/managing_values_of_product_option.feature +++ b/features/product/managing_product_options/managing_values_of_product_option.feature @@ -11,7 +11,7 @@ Feature: Managing option values of a product option And this product option has also the "M" option value with code "OV2" And I am logged in as an administrator - @ui @javascript @api + @ui @mink:chromedriver @api Scenario: Adding an option value to an existing product option When I want to modify the "T-Shirt size" product option And I add the "L" option value identified by "OV3" diff --git a/features/product/managing_product_reviews/deleting_multiple_product_reviews.feature b/features/product/managing_product_reviews/deleting_multiple_product_reviews.feature index bee1555c4c..b6bd4d948f 100644 --- a/features/product/managing_product_reviews/deleting_multiple_product_reviews.feature +++ b/features/product/managing_product_reviews/deleting_multiple_product_reviews.feature @@ -11,7 +11,7 @@ Feature: Deleting multiple product reviews And this product has also a review titled "Cool" and rated 4 with a comment "Quite cool" added by customer "aquaman@dc.com" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple product reviews at once When I browse product reviews And I check the "Awesome" product review diff --git a/features/product/managing_product_reviews/editing_product_review.feature b/features/product/managing_product_reviews/editing_product_review.feature index f91f5918d4..e39aa6ed4b 100644 --- a/features/product/managing_product_reviews/editing_product_review.feature +++ b/features/product/managing_product_reviews/editing_product_review.feature @@ -36,12 +36,12 @@ Feature: Editing product reviews Then I should be notified that it has been successfully edited And this product review rating should be 5 - @ui + @ui @no-api Scenario: Seeing a product's name while editing a product review When I want to modify the "Awesome" product review Then I should be editing review of product "Lamborghini Gallardo Model" - @ui + @ui @no-api Scenario: Seeing a customer's name while editing a product review When I want to modify the "Awesome" product review Then I should see the customer's name "Mike Ross" diff --git a/features/product/managing_product_reviews/filtering_product_reviews.feature b/features/product/managing_product_reviews/filtering_product_reviews.feature new file mode 100644 index 0000000000..047dc30cdb --- /dev/null +++ b/features/product/managing_product_reviews/filtering_product_reviews.feature @@ -0,0 +1,20 @@ +@managing_product_reviews +Feature: Browsing product reviews + In order to work with multiple reviews + As an Administrator + I want to filter product reviews + + Background: + Given the store has customer "Mike Ross" with email "ross@teammike.com" + And the store has a product "Lamborghini Gallardo Model" + And this product has a review titled "Awesome" and rated 5 with a comment "Nice product" added by customer "ross@teammike.com" + And this product has a new review titled "Bad" and rated 1 added by customer "ross@teammike.com" + And I am logged in as an administrator + + @ui @api + Scenario: Browsing accepted reviews + When I want to browse product reviews + And I choose "accepted" as a status filter + And I filter + Then I should see a single product review in the list + And I should see the product review "Awesome" in the list diff --git a/features/product/managing_product_reviews/recalculating_product_average_rating.feature b/features/product/managing_product_reviews/recalculating_product_average_rating.feature index 0eba93af19..d3564507ad 100644 --- a/features/product/managing_product_reviews/recalculating_product_average_rating.feature +++ b/features/product/managing_product_reviews/recalculating_product_average_rating.feature @@ -11,7 +11,7 @@ Feature: Recalculating product average rating And this product has a review titled "Not bad" and rated 3 with a comment "Not bad car" added by customer "specter@teamharvey.com" And I am logged in as an administrator - @ui @javascript + @ui @javascript @api Scenario: Product's average rating is correctly recalculated after review's rate change When I want to modify the "Awesome" product review And I choose 5 as its rating @@ -19,7 +19,7 @@ Feature: Recalculating product average rating Then I should be notified that it has been successfully edited And average rating of product "Lamborghini Gallardo Model" should be 4 - @ui + @ui @api Scenario: Product's average rating is correctly recalculated after review's rate change When I delete the "Awesome" product review Then I should be notified that it has been successfully deleted diff --git a/features/product/managing_product_variants/adding_product_variant.feature b/features/product/managing_product_variants/adding_product_variant.feature index 482b1a9cf5..efaac6f00c 100644 --- a/features/product/managing_product_variants/adding_product_variant.feature +++ b/features/product/managing_product_variants/adding_product_variant.feature @@ -11,17 +11,18 @@ Feature: Adding a new product variant And the store has "Fragile" shipping category And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new product variant When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" And I set its price to "$100.00" for "United States" channel + And I set its "Taste" option to "Orange" And I add it Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should appear in the store - And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at $100.00 for channel "United States" + And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at "$100.00" for channel "United States" - @ui + @api @ui Scenario: Adding a new product variant with name Given the store is also available in "Polish (Poland)" When I want to create a new variant of this product @@ -29,14 +30,15 @@ Feature: Adding a new product variant And I name it "Vodka Wyborowa Premium" in "English (United States)" And I name it "Wódka Wyborowa Premium" in "Polish (Poland)" And I set its price to "$100.00" for "United States" channel + And I set its "Taste" option to "Orange" And I add it Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should appear in the store - And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at $100.00 for channel "United States" + And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at "$100.00" for channel "United States" And the variant with code "VODKA_WYBOROWA_PREMIUM" should be named "Vodka Wyborowa Premium" in "English (United States)" locale And the variant with code "VODKA_WYBOROWA_PREMIUM" should be named "Wódka Wyborowa Premium" in "Polish (Poland)" locale - @ui + @api @ui Scenario: Adding a new product variant with specific option's value When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_MELON" @@ -46,56 +48,61 @@ Feature: Adding a new product variant Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_MELON" variant of the "Wyborowa Vodka" product should appear in the store - @ui + @api @ui Scenario: Adding a new product variant with specific shipping category When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" And I set its price to "$100.00" for "United States" channel And I set its shipping category as "Fragile" + And I set its "Taste" option to "Orange" And I add it Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should appear in the store - @ui + @api @ui Scenario: Adding a new product variant with discounted price When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_EXQUISITE" And I set its price to "$100.00" for "United States" channel And I set its original price to "$120.00" for "United States" channel + And I set its "Taste" option to "Orange" And I add it Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_EXQUISITE" variant of the "Wyborowa Vodka" product should appear in the store - And the variant with code "VODKA_WYBOROWA_EXQUISITE" should be priced at $100.00 for channel "United States" - And the variant with code "VODKA_WYBOROWA_EXQUISITE" should have an original price of $120.00 for channel "United States" + And the variant with code "VODKA_WYBOROWA_EXQUISITE" should be priced at "$100.00" for channel "United States" + And the variant with code "VODKA_WYBOROWA_EXQUISITE" should have an original price of "$120.00" for channel "United States" - @ui + @api @ui Scenario: Adding a new product variant without shipping required When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" And I set its price to "$100.00" for "United States" channel - And I do not want to have shipping required for this product + And I do not want to have shipping required for this product variant + And I set its "Taste" option to "Orange" And I add it Then I should be notified that it has been successfully created And the variant with code "VODKA_WYBOROWA_PREMIUM" should not have shipping required And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should appear in the store - And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at $100.00 for channel "United States" + And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at "$100.00" for channel "United States" - @ui + @api @ui Scenario: Adding a new free product variant When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" And I set its price to "$0.00" for "United States" channel + And I set its "Taste" option to "Orange" And I add it Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should appear in the store - And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at $0.00 for channel "United States" + And the variant with code "VODKA_WYBOROWA_PREMIUM" should be priced at "$0.00" for channel "United States" - @ui @api + @api @ui Scenario: Adding a new product variant with minimum price When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA" And I set its price to "$100.00" for "United States" channel And I set its minimum price to "$50.00" for "United States" channel + And I set its "Taste" option to "Orange" And I add it Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA" variant of the "Wyborowa Vodka" product should appear in the store diff --git a/features/product/managing_product_variants/adding_product_variant_with_only_original_price.feature b/features/product/managing_product_variants/adding_product_variant_with_only_original_price.feature index 7021a2b985..051607d794 100644 --- a/features/product/managing_product_variants/adding_product_variant_with_only_original_price.feature +++ b/features/product/managing_product_variants/adding_product_variant_with_only_original_price.feature @@ -10,7 +10,7 @@ Feature: Adding a product variant with only original price And this product is disabled in "United States" channel And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new product variant without price When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_DELUX" @@ -18,4 +18,4 @@ Feature: Adding a product variant with only original price And I add it Then I should be notified that it has been successfully created And the "VODKA_WYBOROWA_DELUX" variant of the "Wyborowa Vodka" product should appear in the store - And the variant with code "VODKA_WYBOROWA_DELUX" should be originally priced at $100.00 for channel "United States" + And the variant with code "VODKA_WYBOROWA_DELUX" should be originally priced at "$100.00" for channel "United States" diff --git a/features/product/managing_product_variants/browsing_product_variants.feature b/features/product/managing_product_variants/browsing_product_variants.feature index 77b9be9430..506a6f1f7a 100644 --- a/features/product/managing_product_variants/browsing_product_variants.feature +++ b/features/product/managing_product_variants/browsing_product_variants.feature @@ -10,23 +10,23 @@ Feature: Browsing product variants And the product "Wyborowa Vodka" has "Wyborowa Vodka Exquisite" variant priced at "$40.00" And I am logged in as an administrator - @ui + @api @ui Scenario: Browsing product variants in store When I want to view all variants of this product Then I should see 1 variant in the list - @ui + @api @ui Scenario: Being informed that product variant is not tracked When I want to view all variants of this product Then I should see that the "Wyborowa Vodka Exquisite" variant is not tracked - @ui + @api @ui Scenario: Being informed about on hand quantity of a product variant Given the "Wyborowa Vodka Exquisite" product variant is tracked by the inventory When I want to view all variants of this product Then I should see that the "Wyborowa Vodka Exquisite" variant has zero on hand quantity - @ui + @api @ui Scenario: Being informed that product variant is enabled When I want to view all variants of this product Then I should see that the "Wyborowa Vodka Exquisite" variant is enabled diff --git a/features/product/managing_product_variants/checking_products_in_the_shop_while_editing_their_product_variants.feature b/features/product/managing_product_variants/checking_products_in_the_shop_while_editing_their_product_variants.feature index a032bb4426..08dc47350a 100644 --- a/features/product/managing_product_variants/checking_products_in_the_shop_while_editing_their_product_variants.feature +++ b/features/product/managing_product_variants/checking_products_in_the_shop_while_editing_their_product_variants.feature @@ -13,14 +13,14 @@ In order to check a product in shop in all channels it is available in @ui @no-api Scenario: Accessing product show page in shop from the product variant edit page where product is available in more than one channel Given this product is also available in the "Europe" channel - And this product has "Red" variant priced at "$220000.00" in "Europe" channel + And this product has "Red" variant priced at "$220,000.00" in "Europe" channel When I want to modify the "Bugatti" product variant And I choose to show this product in the "Europe" channel Then I should see this product in the "Europe" channel in the shop @ui @no-api Scenario: Accessing product show page in shop from the product variant edit page where product is available in one channel - Given this product has "Red" variant priced at "$220000.00" in "United States" channel + Given this product has "Red" variant priced at "$220,000.00" in "United States" channel When I want to modify the "Bugatti" product variant And I choose to show this product in this channel Then I should see this product in the "United States" channel in the shop diff --git a/features/product/managing_product_variants/deleting_multiple_product_variants.feature b/features/product/managing_product_variants/deleting_multiple_product_variants.feature index 3efe4c5af0..7add26f9ef 100644 --- a/features/product/managing_product_variants/deleting_multiple_product_variants.feature +++ b/features/product/managing_product_variants/deleting_multiple_product_variants.feature @@ -10,7 +10,7 @@ Feature: Deleting multiple product variants And this product has "Small PHP Mug", "Medium PHP Mug" and "Big PHP Mug" variants And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple product variants at once When I browse variants of this product And I check the "Small PHP Mug" product variant diff --git a/features/product/managing_product_variants/deleting_product_variant.feature b/features/product/managing_product_variants/deleting_product_variant.feature index 5f3dfde191..9137386760 100644 --- a/features/product/managing_product_variants/deleting_product_variant.feature +++ b/features/product/managing_product_variants/deleting_product_variant.feature @@ -10,8 +10,8 @@ Feature: Deleting a product variant And the product "PHP Mug" has "Medium PHP Mug" variant priced at "$40.00" And I am logged in as an administrator - @domain @ui - Scenario: Deleted variant disappears from the product catalog + @api @domain @ui + Scenario: Deleting a product variant from the product catalog When I delete the "Medium PHP Mug" variant of product "PHP Mug" Then I should be notified that it has been successfully deleted And this variant should not exist in the product catalog diff --git a/features/product/managing_product_variants/editing_product_variant.feature b/features/product/managing_product_variants/editing_product_variant.feature new file mode 100644 index 0000000000..4fa09c9046 --- /dev/null +++ b/features/product/managing_product_variants/editing_product_variant.feature @@ -0,0 +1,33 @@ +@managing_product_variants +Feature: Editing a product variant + In order to change product variant details + As an Administrator + I want to be able to edit a product variant + + Background: + Given the store operates on a single channel in "United States" + And this channel allows to shop using "English (United States)" and "Polish (Poland)" locales + And the store has a "T-Shirt" configurable product + And this product has option "Size" with values "S", "M" and "L" + And this product has "Go" variant priced at "$100.00" configured with "S" option value + And this product is named "Go" in the "English (United States)" locale + And this product is named "Idź" in the "Polish (Poland)" locale + And I am logged in as an administrator + + @api @ui + Scenario: Changing product variant name + When I want to modify the "Go" product variant + And I name it "Java" in "English (United States)" + And I name it "Kawa" in "Polish (Poland)" + And I save my changes + Then I should be notified that it has been successfully edited + And the variant with code "GO" should be named "Java" in "English (United States)" locale + And the variant with code "GO" should be named "Kawa" in "Polish (Poland)" locale + + @api @ui + Scenario: Changing product variant option values + When I want to modify the "Go" product variant + And I change its "Size" option to "L" + And I save my changes + Then I should be notified that it has been successfully edited + And the variant "Go" should have "Size" option as "L" diff --git a/features/product/managing_product_variants/generating_product_variant.feature b/features/product/managing_product_variants/generating_product_variant.feature index b134ff572e..f16af1078f 100644 --- a/features/product/managing_product_variants/generating_product_variant.feature +++ b/features/product/managing_product_variants/generating_product_variant.feature @@ -10,37 +10,37 @@ Feature: Generating product variants And this product has option "Taste" with values "Orange" and "Melon" And I am logged in as an administrator - @ui + @ui @no-api Scenario: Generating a product variant for product without variants When I want to generate new variants for this product - And I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code and costs "$90" in "United States" channel - And I specify that the 2nd variant is identified by "WYBOROWA_MELON" code and costs "$95" in "United States" channel + And I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code and costs "$90.00" in "United States" channel + And I specify that the 2nd variant is identified by "WYBOROWA_MELON" code and costs "$95.00" in "United States" channel And I generate it Then I should be notified that it has been successfully generated And I should see 2 variants in the list - @ui + @ui @no-api Scenario: Generating the rest of product variants for product with at least one Given this product is available in "Melon" taste priced at "$95.00" When I want to generate new variants for this product - And I specify that the 2nd variant is identified by "WYBOROWA_ORANGE" code and costs "$90" in "United States" channel + And I specify that the 2nd variant is identified by "WYBOROWA_ORANGE" code and costs "$90.00" in "United States" channel And I generate it Then I should be notified that it has been successfully generated And I should see 2 variants in the list - @ui + @ui @no-api Scenario: Generating the rest of product variants for product with at least one Given this product is available in "Orange" taste priced at "$90.00" When I want to generate new variants for this product - And I specify that the 2nd variant is identified by "WYBOROWA_MELON" code and costs "$95" in "United States" channel + And I specify that the 2nd variant is identified by "WYBOROWA_MELON" code and costs "$95.00" in "United States" channel And I generate it Then I should be notified that it has been successfully generated And I should see 2 variants in the list - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Generating only a part of product variants When I want to generate new variants for this product - And I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code and costs "$90" in "United States" channel + And I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code and costs "$90.00" in "United States" channel And I remove 2nd variant from the list And I generate it Then I should be notified that it has been successfully generated diff --git a/features/product/managing_product_variants/generating_product_variant_validation.feature b/features/product/managing_product_variants/generating_product_variant_validation.feature index 68e3e2867b..80272db012 100644 --- a/features/product/managing_product_variants/generating_product_variant_validation.feature +++ b/features/product/managing_product_variants/generating_product_variant_validation.feature @@ -10,7 +10,7 @@ Feature: Generating product variant generation And this product has option "Taste" with values "Orange" and "Melon" And I am logged in as an administrator - @ui + @ui @no-api Scenario: Generating a product's variant without price When I want to generate new variants for this product And I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code @@ -18,37 +18,37 @@ Feature: Generating product variant generation Then I should be notified that prices in all channels must be defined for the 1st variant And I should not see any variants in the list - @ui + @ui @no-api Scenario: Generating a product's variant without code When I want to generate new variants for this product - And I specify that the 1st variant costs "$90" in "United States" channel + And I specify that the 1st variant costs "$90.00" in "United States" channel And I try to generate it Then I should be notified that code is required for the 1st variant And I should not see any variants in the list - @ui + @ui @no-api Scenario: Generating product's variants without specific required fields for second variant When I want to generate new variants for this product And I specify that the 1st variant is identified by "WYBOROWA_ORANGE" code - And I specify that the 1st variant costs "$90" in "United States" channel + And I specify that the 1st variant costs "$90.00" in "United States" channel And I try to generate it Then I should be notified that code is required for the 2nd variant Then I should be notified that prices in all channels must be defined for the 2nd variant And I should not see any variants in the list - @ui + @ui @no-api Scenario: Generating product's variants with the same code When I want to generate new variants for this product And I specify that the 1st variant is identified by "WYBOROWA_TASTE" code - And I specify that the 1st variant costs "$90" in "United States" channel + And I specify that the 1st variant costs "$90.00" in "United States" channel And I specify that the 2nd variant is identified by "WYBOROWA_TASTE" code - And I specify that the 2nd variant costs "$90" in "United States" channel + And I specify that the 2nd variant costs "$90.00" in "United States" channel And I try to generate it Then I should be notified that variant code must be unique within this product for the 1st variant And I should be notified that variant code must be unique within this product for the 2nd variant And I should not see any variants in the list - @ui + @ui @no-api Scenario: Generating product's variants without specific required fields for second variant When I want to generate new variants for this product And I do not specify any information about variants diff --git a/features/product/managing_product_variants/prevent_deletion_of_purchased_product_variant.feature b/features/product/managing_product_variants/preventing_deletion_of_purchased_product_variant.feature similarity index 92% rename from features/product/managing_product_variants/prevent_deletion_of_purchased_product_variant.feature rename to features/product/managing_product_variants/preventing_deletion_of_purchased_product_variant.feature index 83f7d4a007..c14a2378c5 100644 --- a/features/product/managing_product_variants/prevent_deletion_of_purchased_product_variant.feature +++ b/features/product/managing_product_variants/preventing_deletion_of_purchased_product_variant.feature @@ -15,8 +15,8 @@ Feature: Prevent deletion of purchased product variant And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @domain @ui - Scenario: Purchased product variant cannot be deleted + @api @domain @ui + Scenario: Being unable to delete a purchased product variant When I try to delete the "Medium PHP Mug" variant of product "PHP Mug" Then I should be notified that this variant is in use and cannot be deleted And this variant should still exist in the product catalog diff --git a/features/product/managing_product_variants/preventing_the_generation_of_products_variants_from_options_without_any_values.feature b/features/product/managing_product_variants/preventing_generation_of_products_variants_from_options_without_any_values.feature similarity index 98% rename from features/product/managing_product_variants/preventing_the_generation_of_products_variants_from_options_without_any_values.feature rename to features/product/managing_product_variants/preventing_generation_of_products_variants_from_options_without_any_values.feature index 2e7a71af67..677bb405d3 100644 --- a/features/product/managing_product_variants/preventing_the_generation_of_products_variants_from_options_without_any_values.feature +++ b/features/product/managing_product_variants/preventing_generation_of_products_variants_from_options_without_any_values.feature @@ -10,7 +10,7 @@ Feature: Preventing the generation of product variants from options without any And this product has an option "Taste" without any values And I am logged in as an administrator - @ui + @ui @no-api Scenario: Trying to generate a product variant for a product without options values When I try to generate new variants for this product Then I should be notified that variants cannot be generated from options without any values diff --git a/features/product/managing_product_variants/product_variant_validation.feature b/features/product/managing_product_variants/product_variant_validation.feature index 224abad4d7..78185cc52e 100644 --- a/features/product/managing_product_variants/product_variant_validation.feature +++ b/features/product/managing_product_variants/product_variant_validation.feature @@ -9,7 +9,7 @@ Feature: Product variant validation And the store has a "Wyborowa Vodka" configurable product And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new product variant without specifying its price When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" @@ -18,16 +18,16 @@ Feature: Product variant validation Then I should be notified that prices in all channels must be defined And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should not appear in the store - @ui + @api @ui Scenario: Adding a new product variant with price below 0 When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" - And I set its price to "$-60.00" for "United States" channel + And I set its price to "-$60.00" for "United States" channel And I try to add it Then I should be notified that price cannot be lower than 0 And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should not appear in the store - @ui + @api @ui Scenario: Adding a new product variant without specifying its code When I want to create a new variant of this product And I set its price to "$80.00" for "United States" channel @@ -36,9 +36,9 @@ Feature: Product variant validation Then I should be notified that code is required And the "Wyborowa Vodka" product should have no variants - @ui + @api @ui Scenario: Adding a new product variant with duplicated code - Given this product has "Wyborowa Exquisite" variant priced at "$90" identified by "VODKA_WYBOROWA_PREMIUM" + Given this product has "Wyborowa Exquisite" variant priced at "$90.00" identified by "VODKA_WYBOROWA_PREMIUM" When I want to create a new variant of this product And I set its price to "$80.00" for "United States" channel And I specify its code as "VODKA_WYBOROWA_PREMIUM" @@ -46,7 +46,7 @@ Feature: Product variant validation Then I should be notified that code has to be unique And the "Wyborowa Vodka" product should have only one variant - @ui + @api @ui Scenario: Adding a new product variant with same set of options Given this product has option "Taste" with values "Orange" and "Melon" And this product is available in "Melon" taste priced at "$95.00" @@ -58,7 +58,44 @@ Feature: Product variant validation Then I should be notified that this variant already exists And the "Wyborowa Vodka" product should have only one variant - @ui + @api @no-ui + Scenario: Adding a new product variant with two values of the same option + Given this product has option "Taste" with values "Orange" and "Melon" + When I want to create a new variant of this product + And I specify its code as "VODKA_WYBOROWA_PREMIUM" + And I set its "Taste" option to "Orange" + And I set its "Taste" option to "Melon" + And I set its price to "$100.00" for "United States" channel + And I try to add it + Then I should be notified that the variant can have only one value configured for a single option + And the "Wyborowa Vodka" product should have no variants + + @api @no-ui + Scenario: Adding a new product variant without any of required options configured + Given this product has option "Taste" with values "Orange" and "Melon" + And this product has option "Size" with values "Small" and "Big" + When I want to create a new variant of this product + And I specify its code as "VODKA_WYBOROWA_PREMIUM" + And I do not set its "Taste" and "Size" options + And I set its price to "$100.00" for "United States" channel + And I try to add it + Then I should be notified that required options have not been configured + And the "Wyborowa Vodka" product should have no variants + + @api @no-ui + Scenario: Adding a new product variant without one of required options configured + Given this product has option "Taste" with values "Orange" and "Melon" + And this product has option "Size" with values "Small" and "Big" + When I want to create a new variant of this product + And I specify its code as "VODKA_WYBOROWA_PREMIUM" + And I set its "Taste" option to "Orange" + And I do not set its "Size" option + And I set its price to "$100.00" for "United States" channel + And I try to add it + Then I should be notified that required options have not been configured + And the "Wyborowa Vodka" product should have no variants + + @api @ui Scenario: Adding a new product variant with negative properties When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" @@ -68,7 +105,7 @@ Feature: Product variant validation Then I should be notified that height, width, depth and weight cannot be lower than 0 And the "VODKA_WYBOROWA_PREMIUM" variant of the "Wyborowa Vodka" product should not appear in the store - @ui + @api @ui Scenario: Adding a new product variant without current stock When I want to create a new variant of this product And I specify its code as "VODKA_WYBOROWA_PREMIUM" diff --git a/features/product/managing_product_variants/removing_product_variant_price_from_obsolete_channel.feature b/features/product/managing_product_variants/removing_product_variant_price_from_obsolete_channel.feature index 71ce655773..2beca9f082 100644 --- a/features/product/managing_product_variants/removing_product_variant_price_from_obsolete_channel.feature +++ b/features/product/managing_product_variants/removing_product_variant_price_from_obsolete_channel.feature @@ -8,25 +8,25 @@ Feature: Removing a product variant's price from obsolete channel Given the store operates on a channel named "Web-US" in "USD" currency And the store operates on another channel named "Web-GB" in "USD" currency And the store has a "PHP Mug" configurable product - And this product has "Medium PHP Mug" variant priced at "$20" in "Web-US" channel - And "Medium PHP Mug" variant priced at "£25" in "Web-GB" channel + And this product has "Medium PHP Mug" variant priced at "$20.00" in "Web-US" channel + And "Medium PHP Mug" variant priced at "£25.00" in "Web-GB" channel And "Medium PHP Mug" variant is originally priced at "£50.00" in "Web-GB" channel And this product is disabled in "Web-GB" channel And I am logged in as an administrator - @ui - Scenario: Removing a product variant's price from disabled channel + @api @ui + Scenario: Removing a product variant's price When I want to modify the "Medium PHP Mug" product variant - And I remove its price for "Web-GB" channel + And I remove its price from "Web-GB" channel And I save my changes Then I should not have configured price for "Web-GB" channel But I should have original price equal to "£50.00" in "Web-GB" channel - @ui + @api @ui Scenario: Removing a product variant's price from disabled channel Given the channel "Web-GB" has been disabled When I want to modify the "Medium PHP Mug" product variant - And I remove its price for "Web-GB" channel + And I remove its price from "Web-GB" channel And I save my changes Then I should not have configured price for "Web-GB" channel But I should have original price equal to "£50.00" in "Web-GB" channel diff --git a/features/product/managing_product_variants/seeing_correct_option_values_while_editing_product_variant.feature b/features/product/managing_product_variants/seeing_correct_option_values_while_editing_product_variant.feature index 993d907c57..bb88bcc446 100644 --- a/features/product/managing_product_variants/seeing_correct_option_values_while_editing_product_variant.feature +++ b/features/product/managing_product_variants/seeing_correct_option_values_while_editing_product_variant.feature @@ -12,13 +12,13 @@ Feature: Seeing correct option values while editing product variant And this product has option "Type" with values "Clear" and "Color" And I am logged in as an administrator - @ui + @ui @no-api Scenario: Seeing default option values while editing product variant in store When I want to modify the "Wyborowa Vodka Exquisite" product variant And I should see the "Type" option as "Clear" And I should see the "Taste" option as "Orange" - @ui + @ui @no-api Scenario: Seeing changed option values while editing product variant in store When I want to modify the "Wyborowa Vodka Exquisite" product variant And I change its "Taste" option to "Melon" diff --git a/features/product/managing_product_variants/sorting_product_variants_within_product_by_position.feature b/features/product/managing_product_variants/sorting_product_variants_within_product_by_position.feature index 194378fe31..b3c9a680b4 100644 --- a/features/product/managing_product_variants/sorting_product_variants_within_product_by_position.feature +++ b/features/product/managing_product_variants/sorting_product_variants_within_product_by_position.feature @@ -12,44 +12,46 @@ Feature: Sorting listed product variants from a product by position And this product has also an "Opel Insignia Sedan" variant at position 1 And I am logged in as an administrator - @ui + @ui @api Scenario: Product variants are sorted by position in ascending order by default When I view all variants of the product "Opel Insignia" Then I should see 3 variants in the list And the first variant in the list should have name "Opel Insignia Hatchback" And the last variant in the list should have name "Opel Insignia Sports Tourer" - @ui + @ui @api Scenario: Sorting product variants in descending order When I view all variants of the product "Opel Insignia" And I start sorting variants by position Then the first variant in the list should have name "Opel Insignia Sports Tourer" And the last variant in the list should have name "Opel Insignia Hatchback" - @ui + @ui @api Scenario: New product variant with no position is added as the last one Given the product "Opel Insignia" has also an "Opel Insignia Country Tourer" variant When I view all variants of the product "Opel Insignia" Then I should see 4 variants in the list And the last variant in the list should have name "Opel Insignia Country Tourer" - @ui + @ui @api Scenario: New product variant with position 0 is added as the first one Given the product "Opel Insignia" has also an "Opel Insignia Country Tourer" variant at position 0 When I view all variants of the product "Opel Insignia" Then I should see 4 variants in the list And the first variant in the list should have name "Opel Insignia Country Tourer" - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Setting product variant as the first one in the list When I view all variants of the product "Opel Insignia" And I set the position of "Opel Insignia Sedan" to 0 And I save my new configuration - And the first variant in the list should have name "Opel Insignia Sedan" + And I view all variants of the product "Opel Insignia" again + Then the first variant in the list should have name "Opel Insignia Sedan" - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Setting product variant as the last one in the list When I view all variants of the product "Opel Insignia" And I set the position of "Opel Insignia Sedan" to 7 And I save my new configuration - And the last variant in the list should have name "Opel Insignia Sedan" + And I view all variants of the product "Opel Insignia" again + Then the last variant in the list should have name "Opel Insignia Sedan" diff --git a/features/product/managing_product_variants/disable_or_enable_inventory_on_product_variant.feature b/features/product/managing_product_variants/toggling_inventory_tracking_on_product_variant.feature similarity index 84% rename from features/product/managing_product_variants/disable_or_enable_inventory_on_product_variant.feature rename to features/product/managing_product_variants/toggling_inventory_tracking_on_product_variant.feature index 343c815323..4f93e74b45 100644 --- a/features/product/managing_product_variants/disable_or_enable_inventory_on_product_variant.feature +++ b/features/product/managing_product_variants/toggling_inventory_tracking_on_product_variant.feature @@ -1,5 +1,5 @@ @managing_product_variants -Feature: Toggle the inventory tracking +Feature: Toggling the inventory tracking In order to have the inventory tracked in my shop As an Administrator I want to toggle the inventory tracking @@ -10,8 +10,8 @@ Feature: Toggle the inventory tracking And the product "Wyborowa Vodka" has a "Wyborowa Vodka Exquisite" variant priced at "$40.00" And I am logged in as an administrator - @ui - Scenario: Disabling inventory for a product variant + @api @ui + Scenario: Disabling inventory tracking for the product variant Given the "Wyborowa Vodka Exquisite" product variant is tracked by the inventory When I want to modify the "Wyborowa Vodka Exquisite" product variant And I disable its inventory tracking @@ -19,8 +19,8 @@ Feature: Toggle the inventory tracking Then I should be notified that it has been successfully edited And inventory of this variant should not be tracked - @ui - Scenario: Enabling inventory for a product variant + @api @ui + Scenario: Enabling inventory tracking for the product variant When I want to modify the "Wyborowa Vodka Exquisite" product variant And I enable its inventory tracking And I save my changes diff --git a/features/product/managing_product_variants/disable_or_enable_product_variant.feature b/features/product/managing_product_variants/toggling_product_variant.feature similarity index 87% rename from features/product/managing_product_variants/disable_or_enable_product_variant.feature rename to features/product/managing_product_variants/toggling_product_variant.feature index a54e2f15a3..8a7f37757e 100644 --- a/features/product/managing_product_variants/disable_or_enable_product_variant.feature +++ b/features/product/managing_product_variants/toggling_product_variant.feature @@ -1,5 +1,5 @@ @managing_product_variants -Feature: Toggle the product variant +Feature: Toggling the product variant In order to stop or resume the sale of some product variants As an Administrator I want to toggle the product variant @@ -10,8 +10,8 @@ Feature: Toggle the product variant And the product "Wyborowa Vodka" has a "Wyborowa Vodka Exquisite" variant priced at "$40.00" And I am logged in as an administrator - @ui - Scenario: Disabling a product variant + @api @ui + Scenario: Disabling the product variant Given the "Wyborowa Vodka Exquisite" product variant is enabled When I want to modify the "Wyborowa Vodka Exquisite" product variant And I disable it @@ -19,8 +19,8 @@ Feature: Toggle the product variant Then I should be notified that it has been successfully edited And this variant should be disabled - @ui - Scenario: Enabling a product variant + @api @ui + Scenario: Enabling the product variant Given the "Wyborowa Vodka Exquisite" product variant is disabled When I want to modify the "Wyborowa Vodka Exquisite" product variant And I enable it diff --git a/features/product/managing_products/accessing_variants_management_from_product_edit_page.feature b/features/product/managing_products/accessing_variants_management_from_product_edit_page.feature index de3aebb4f3..f3084b9a8e 100644 --- a/features/product/managing_products/accessing_variants_management_from_product_edit_page.feature +++ b/features/product/managing_products/accessing_variants_management_from_product_edit_page.feature @@ -10,19 +10,19 @@ Feature: Accessing the variants management from the product edit page And this product has option "Model" with values "RS6" and "RS7" And I am logged in as an administrator - @ui + @ui @no-api Scenario: Being able to access the variants list page When I modify the "Audi" product And I go to the variants list Then I should be on the list of this product's variants - @ui + @ui @no-api Scenario: Being able to access the variant creation page When I modify the "Audi" product And I go to the variant creation page Then I should be on the variant creation page for this product - @ui + @ui @no-api Scenario: Being able to access the variant generation page When I modify the "Audi" product And I go to the variant generation page diff --git a/features/product/managing_products/adding_associations_to_existing_product.feature b/features/product/managing_products/adding_associations_to_existing_product.feature index e63f35aa27..ab3d2726a7 100644 --- a/features/product/managing_products/adding_associations_to_existing_product.feature +++ b/features/product/managing_products/adding_associations_to_existing_product.feature @@ -9,10 +9,16 @@ Feature: Adding associations to an existing product And the store has "LG G3", "LG headphones" and "LG earphones" products And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Adding an association to an existing product When I want to modify the "LG G3" product And I associate as "Accessories" the "LG headphones" and "LG earphones" products And I save my changes Then I should be notified that it has been successfully edited And this product should have an association "Accessories" with products "LG headphones" and "LG earphones" + + @api @no-ui + Scenario: Adding an association to na existing product + When I associate as "Accessories" the product "LG G3" with the products "LG headphones" and "LG earphones" + And this product should have an association "Accessories" + And this association should have products "LG headphones" and "LG earphones" diff --git a/features/product/managing_products/adding_images_to_existing_product.feature b/features/product/managing_products/adding_images_to_existing_product.feature index 67f3f606fd..4961898698 100644 --- a/features/product/managing_products/adding_images_to_existing_product.feature +++ b/features/product/managing_products/adding_images_to_existing_product.feature @@ -5,71 +5,83 @@ Feature: Adding images to an existing product I want to be able to add new images to a taxon Background: - Given I am logged in as an administrator + Given the store operates on a single channel in "United States" + And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding a single image to an existing product Given the store has a product "Lamborghini Gallardo Model" When I want to modify this product - And I attach the "lamborghini.jpg" image with "banner" type - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "lamborghini.jpg" image with "banner" type to this product + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And the product "Lamborghini Gallardo Model" should have an image with "banner" type - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding multiple images to an existing product Given the store has a product "Lamborghini Gallardo Model" When I want to modify this product - And I attach the "lamborghini.jpg" image with "banner" type - And I attach the "lamborghini.jpg" image with "thumbnail" type - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "lamborghini.jpg" image with "banner" type to this product + And I attach the "lamborghini.jpg" image with "thumbnail" type to this product + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And the product "Lamborghini Gallardo Model" should have an image with "banner" type And it should also have an image with "thumbnail" type - @ui @javascript + @ui @javascript @api Scenario: Adding multiple images of the same type to an existing product Given the store has a product "Lamborghini Ford Model" When I want to modify this product - And I attach the "lamborghini.jpg" image with "banner" type - And I attach the "ford.jpg" image with "banner" type - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "lamborghini.jpg" image with "banner" type to this product + And I attach the "ford.jpg" image with "banner" type to this product + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And this product should have 2 images - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding a single image to an existing configurable product Given the store has a "Lamborghini Gallardo Model" configurable product When I want to modify this product - And I attach the "lamborghini.jpg" image with "banner" type - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "lamborghini.jpg" image with "banner" type to this product + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And the product "Lamborghini Gallardo Model" should have an image with "banner" type - @ui @javascript + @ui @javascript @api Scenario: Adding multiple images of the same type to an existing configurable product Given the store has a "Lamborghini Ford Model" configurable product When I want to modify this product - And I attach the "lamborghini.jpg" image with "banner" type - And I attach the "ford.jpg" image with "banner" type - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "lamborghini.jpg" image with "banner" type to this product + And I attach the "ford.jpg" image with "banner" type to this product + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And this product should have 2 images - @ui @javascript + @ui @javascript @api Scenario: Adding an image to an existing product without providing its type Given the store has a product "Lamborghini Gallardo Model" When I want to modify this product - And I attach the "lamborghini.jpg" image - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "lamborghini.jpg" image to this product + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And this product should have only one image - @ui @javascript + @ui @javascript @api Scenario: Adding an image to an existing configurable product without providing its type Given the store has a "Lamborghini Gallardo Model" configurable product When I want to modify this product - And I attach the "lamborghini.jpg" image - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "lamborghini.jpg" image to this product + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And this product should have only one image + + @ui @mink:chromedriver @api + Scenario: Adding an image to an existing configurable product with selecting a variant + Given the store has a "Lamborghini Gallardo Model" configurable product + And this product has "Blue" and "Yellow" variants + When I want to modify this product + And I attach the "lamborghini.jpg" image with selected "Yellow" variant to this product + And I save my changes to the images + Then I should be notified that it has been successfully uploaded + And this product should have only one image + And its image should have "Yellow" variant selected diff --git a/features/product/managing_products/adding_multiple_product_attributes_at_once.feature b/features/product/managing_products/adding_multiple_product_attributes_at_once.feature index 33a8282f89..1a4cf716ae 100644 --- a/features/product/managing_products/adding_multiple_product_attributes_at_once.feature +++ b/features/product/managing_products/adding_multiple_product_attributes_at_once.feature @@ -10,12 +10,11 @@ Feature: Adding a new integer product attribute And the store has a non-translatable text product attribute "ISBN" And I am logged in as an administrator - @ui @javascript @no-api + @ui @javascript @api Scenario: Adding two text attributes to a product - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "HARRY_POTTER_1" And I name it "Harry Potter and the Sorcerer's Stone" in "English (United States)" - And I set its price to "$10.99" for "United States" channel And I set its non-translatable "Author" attribute to "J.K. Rowling" And I set its non-translatable "ISBN" attribute to "978-1338878929" And I add it diff --git a/features/product/managing_products/adding_product.feature b/features/product/managing_products/adding_product.feature index 7a64c1e26b..0c19302a3f 100644 --- a/features/product/managing_products/adding_product.feature +++ b/features/product/managing_products/adding_product.feature @@ -55,7 +55,7 @@ Feature: Adding a new product Then I should be notified that it has been successfully created And the product "Dice Brewing" should appear in the store - @ui @javascript + @ui @javascript @api Scenario: Adding a new configurable product Given the store has a product option "Bottle size" with a code "bottle_size" And this product option has the "0.7" option value with code "bottle_size_medium" diff --git a/features/product/managing_products/adding_product_with_associations.feature b/features/product/managing_products/adding_product_with_associations.feature index 10bd4d63de..c80f2428bd 100644 --- a/features/product/managing_products/adding_product_with_associations.feature +++ b/features/product/managing_products/adding_product_with_associations.feature @@ -38,7 +38,7 @@ Feature: Adding a new product with associations And this product should not have an association "Accessories" with product "LG earphones" And the product "LG G3" should appear in the store - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Adding a new product with association with numeric code Given the store has 123 product association type When I want to create a new simple product diff --git a/features/product/managing_products/adding_product_with_date_attribute.feature b/features/product/managing_products/adding_product_with_date_attribute.feature new file mode 100644 index 0000000000..e92a8778be --- /dev/null +++ b/features/product/managing_products/adding_product_with_date_attribute.feature @@ -0,0 +1,31 @@ +@managing_products +Feature: Adding a new product with a date attribute + In order to extend my merchandise with more complex products + As an Administrator + I want to add a new product with a date attribute to the shop + + Background: + Given the store operates on a single channel in "United States" + And the store has a non-translatable date product attribute "Manufactured" with format "Y-m-d" + And I am logged in as an administrator + + @api + Scenario: Adding a date attribute to a product + When I want to create a new configurable product + And I specify its code as "mug" + And I name it "Mug" in "English (United States)" + And I set its non-translatable "Manufactured" attribute to "2023-10-10" + And I add it + Then I should be notified that it has been successfully created + And the product "Mug" should appear in the store + And non-translatable attribute "Manufactured" of product "Mug" should be "2023-10-10" + + @api @no-ui + Scenario: Trying to add an invalid date attribute to product + When I want to create a new configurable product + And I specify its code as "44_MAGNUM" + And I name it "44 Magnum" in "English (United States)" + And I set the invalid integer value of the non-translatable "Manufactured" attribute to 10 + And I try to add it + Then I should be notified that the value of the "Manufactured" attribute has invalid type + And product with code "44_MAGNUM" should not be added diff --git a/features/product/managing_products/adding_product_with_datetime_attribute.feature b/features/product/managing_products/adding_product_with_datetime_attribute.feature new file mode 100644 index 0000000000..3e12d3333d --- /dev/null +++ b/features/product/managing_products/adding_product_with_datetime_attribute.feature @@ -0,0 +1,31 @@ +@managing_products +Feature: Adding a new product with a datetime attribute + In order to extend my merchandise with more complex products + As an Administrator + I want to add a new product with a datetime attribute to the shop + + Background: + Given the store operates on a single channel in "United States" + And the store has a non-translatable datetime product attribute "Manufactured" with format "Y-m-d" + And I am logged in as an administrator + + @api + Scenario: Adding a datetime attribute to a product + When I want to create a new configurable product + And I specify its code as "mug" + And I name it "Mug" in "English (United States)" + And I set its non-translatable "Manufactured" attribute to "2023-10-10 10:20:30" + And I add it + Then I should be notified that it has been successfully created + And the product "Mug" should appear in the store + And non-translatable attribute "Manufactured" of product "Mug" should be "2023-10-10 10:20:30" + + @api @no-ui + Scenario: Trying to add an invalid datetime attribute to product + When I want to create a new configurable product + And I specify its code as "44_MAGNUM" + And I name it "44 Magnum" in "English (United States)" + And I set the invalid integer value of the non-translatable "Manufactured" attribute to 10 + And I try to add it + Then I should be notified that the value of the "Manufactured" attribute has invalid type + And product with code "44_MAGNUM" should not be added diff --git a/features/product/managing_products/adding_product_with_float_attribute.feature b/features/product/managing_products/adding_product_with_float_attribute.feature new file mode 100644 index 0000000000..8d19c932b0 --- /dev/null +++ b/features/product/managing_products/adding_product_with_float_attribute.feature @@ -0,0 +1,31 @@ +@managing_products +Feature: Adding a new product with a float attribute + In order to extend my merchandise with more complex products + As an Administrator + I want to add a new product with a float attribute to the shop + + Background: + Given the store operates on a single channel in "United States" + And the store has a non-translatable float product attribute "Display Size" + And I am logged in as an administrator + + @ui @javascript @api + Scenario: Adding a float attribute to a product + When I want to create a new configurable product + And I specify its code as "display_size" + And I name it "Smartphone" in "English (United States)" + And I set its non-translatable "Display Size" attribute to 12.5 + And I add it + Then I should be notified that it has been successfully created + And the product "Smartphone" should appear in the store + And non-translatable attribute "Display Size" of product "Smartphone" should be 12.5 + + @api @no-ui + Scenario: Trying to add an invalid float attribute to product + When I want to create a new configurable product + And I specify its code as "44_MAGNUM" + And I name it "44 Magnum" in "English (United States)" + And I set the invalid string value of the non-translatable "Display Size" attribute to "12.5" + And I try to add it + Then I should be notified that the value of the "Display Size" attribute has invalid type + And product with code "44_MAGNUM" should not be added diff --git a/features/product/managing_products/adding_product_with_images.feature b/features/product/managing_products/adding_product_with_images.feature index 153065ae81..f44667ef09 100644 --- a/features/product/managing_products/adding_product_with_images.feature +++ b/features/product/managing_products/adding_product_with_images.feature @@ -32,7 +32,7 @@ Feature: Adding a new product with images And the product "Lamborghini Gallardo Model" should have an image with "banner" type And it should also have an image with "thumbnail" type - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Adding a new configurable product with a single image Given the store has a product option "Model scale" with a code "model_scale" And this product option has the "1:43" option value with code "model_scale_medium" diff --git a/features/product/managing_products/adding_product_with_integer_attribute.feature b/features/product/managing_products/adding_product_with_integer_attribute.feature index 7cebccaf70..4226470be4 100644 --- a/features/product/managing_products/adding_product_with_integer_attribute.feature +++ b/features/product/managing_products/adding_product_with_integer_attribute.feature @@ -10,26 +10,34 @@ Feature: Adding a new product with an integer attribute And the store has a non-translatable integer product attribute "Weight" And I am logged in as an administrator - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Adding an integer attribute to product - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "44_MAGNUM" And I name it "44 Magnum" in "English (United States)" - And I set its price to "$100.00" for "United States" channel - And I set its "Production year" attribute to "1955" in "English (United States)" + And I set its "Production year" attribute to 1955 in "English (United States)" And I add it Then I should be notified that it has been successfully created And the product "44 Magnum" should appear in the store - And attribute "Production year" of product "44 Magnum" should be "1955" + And attribute "Production year" of product "44 Magnum" should be 1955 - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Adding an integer non-translatable attribute to product - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "44_MAGNUM" And I name it "44 Magnum" in "English (United States)" - And I set its price to "$100.00" for "United States" channel - And I set its non-translatable "Weight" attribute to "10" + And I set its non-translatable "Weight" attribute to 10 And I add it Then I should be notified that it has been successfully created And the product "44 Magnum" should appear in the store - And non-translatable attribute "Weight" of product "44 Magnum" should be "10" + And non-translatable attribute "Weight" of product "44 Magnum" should be 10 + + @api @no-ui + Scenario: Trying to add an invalid integer attribute to product + When I want to create a new configurable product + And I specify its code as "44_MAGNUM" + And I name it "44 Magnum" in "English (United States)" + And I set the invalid string value of the non-translatable "Weight" attribute to "ten" + And I try to add it + Then I should be notified that the value of the "Weight" attribute has invalid type + And product with code "44_MAGNUM" should not be added diff --git a/features/product/managing_products/adding_product_with_percent_attribute.feature b/features/product/managing_products/adding_product_with_percent_attribute.feature index 0722ce5e2f..7063880eb2 100644 --- a/features/product/managing_products/adding_product_with_percent_attribute.feature +++ b/features/product/managing_products/adding_product_with_percent_attribute.feature @@ -10,26 +10,34 @@ Feature: Adding a new product with a percent attribute And the store has a non-translatable percent product attribute "Accuracy" And I am logged in as an administrator - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Adding a percent attribute to product - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "44_MAGNUM" And I name it "44 Magnum" in "English (United States)" - And I set its price to "$100.00" for "United States" channel - And I set its "Awesomeness rating" attribute to "80" in "English (United States)" + And I set its "Awesomeness rating" attribute to 80 in "English (United States)" And I add it Then I should be notified that it has been successfully created And the product "44 Magnum" should appear in the store - And attribute "Awesomeness rating" of product "44 Magnum" should be "80" + And attribute "Awesomeness rating" of product "44 Magnum" should be 80 - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Adding a non-translatable percent attribute to product - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "44_MAGNUM" And I name it "44 Magnum" in "English (United States)" - And I set its price to "$100.00" for "United States" channel - And I set its non-translatable "Accuracy" attribute to "95" + And I set its non-translatable "Accuracy" attribute to 95 And I add it Then I should be notified that it has been successfully created And the product "44 Magnum" should appear in the store - And non-translatable attribute "Accuracy" of product "44 Magnum" should be "95" + And non-translatable attribute "Accuracy" of product "44 Magnum" should be 95 + + @api @no-ui + Scenario: Trying to add an invalid percent attribute to product + When I want to create a new configurable product + And I specify its code as "44_MAGNUM" + And I name it "44 Magnum" in "English (United States)" + And I set the invalid string value of the non-translatable "Accuracy" attribute to "ninety" + And I try to add it + Then I should be notified that the value of the "Accuracy" attribute has invalid type + And product with code "44_MAGNUM" should not be added diff --git a/features/product/managing_products/adding_product_with_select_attribute.feature b/features/product/managing_products/adding_product_with_select_attribute.feature new file mode 100644 index 0000000000..f4b75295ac --- /dev/null +++ b/features/product/managing_products/adding_product_with_select_attribute.feature @@ -0,0 +1,33 @@ +@managing_products +Feature: Adding a new product with a select attribute + In order to extend my merchandise with more complex products + As an Administrator + I want to add a new product with a select attribute to the shop + + Background: + Given the store operates on a single channel in "United States" + And the store has a non-translatable select product attribute "Mug material" with value "Ceramic" + And I am logged in as an administrator + + @ui @javascript @api + Scenario: Adding a product with a select attribute with choices in different locales + When I want to create a new configurable product + And I specify its code as "mug" + And I name it "PHP Mug" in "English (United States)" + And I add the "Mug material" attribute + And I select "Ceramic" value for the "Mug material" attribute + And I add it + Then I should be notified that it has been successfully created + And the product "PHP Mug" should appear in the store + And select attribute "Mug material" of product "PHP Mug" should be "Ceramic" + + @api @no-ui + Scenario: Trying to add an invalid select attribute to product + When I want to create a new configurable product + And I specify its code as "mug" + And I name it "PHP Mug" in "English (United States)" + And I add the "Mug material" attribute + And I set the invalid string value of the non-translatable "Mug material" attribute to "ceramic" + And I add it + Then I should be notified that the value of the "Mug material" attribute has invalid type + And product with code "mug" should not be added diff --git a/features/product/managing_products/adding_product_with_text_attribute.feature b/features/product/managing_products/adding_product_with_text_attribute.feature index e6027f251b..c4ceea69c5 100644 --- a/features/product/managing_products/adding_product_with_text_attribute.feature +++ b/features/product/managing_products/adding_product_with_text_attribute.feature @@ -11,36 +11,33 @@ Feature: Adding a new product with text attribute And the store has a non-translatable text product attribute "Author" And I am logged in as an administrator - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Adding a text attribute to product - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "44_MAGNUM" And I name it "44 Magnum" in "English (United States)" - And I set its price to "$100.00" for "United States" channel And I set its "Gun caliber" attribute to "11 mm" in "English (United States)" And I add it Then I should be notified that it has been successfully created And the product "44 Magnum" should appear in the store And attribute "Gun caliber" of product "44 Magnum" should be "11 mm" - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Adding a non-translatable text attribute to product - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "44_MAGNUM" And I name it "44 Magnum" in "English (United States)" - And I set its price to "$100.00" for "United States" channel And I set its non-translatable "Author" attribute to "Colt" And I add it Then I should be notified that it has been successfully created And the product "44 Magnum" should appear in the store And non-translatable attribute "Author" of product "44 Magnum" should be "Colt" - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Adding and removing text attributes on product create page - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "44_MAGNUM" And I name it "44 Magnum" in "English (United States)" - And I set its price to "$100.00" for "United States" channel And I set its "Gun caliber" attribute to "11 mm" in "English (United States)" And I set its "Overall length" attribute to "30.5 cm" in "English (United States)" And I remove its "Gun caliber" attribute @@ -49,3 +46,13 @@ Feature: Adding a new product with text attribute And the product "44 Magnum" should appear in the store And attribute "Overall length" of product "44 Magnum" should be "30.5 cm" And product "44 Magnum" should not have a "Gun caliber" attribute + + @api @no-ui + Scenario: Trying to add an invalid text attribute to product + When I want to create a new configurable product + And I specify its code as "44_MAGNUM" + And I name it "44 Magnum" in "English (United States)" + And I set the invalid integer value of the non-translatable "Author" attribute to 5 + And I try to add it + Then I should be notified that the value of the "Author" attribute has invalid type + And product with code "44_MAGNUM" should not be added diff --git a/features/product/managing_products/adding_select_attribute_in_different_locales.feature b/features/product/managing_products/adding_select_attribute_in_different_locales.feature index 48fd7fcb0e..403bb75d69 100644 --- a/features/product/managing_products/adding_select_attribute_in_different_locales.feature +++ b/features/product/managing_products/adding_select_attribute_in_different_locales.feature @@ -14,12 +14,11 @@ Feature: Adding select attributes in different locales to a product And this product attribute's "Ceramic" value is labeled "Ceramika" in the "Polish (Poland)" locale And I am logged in as an administrator - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Adding a product with a select attribute with choices in different locales - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "mug" And I name it "PHP Mug" in "English (United States)" - And I set its price to "$100.00" for "Web" channel And I add the "Mug material" attribute And I select "Ceramic" value in "English (United States)" for the "Mug material" attribute And I select "Ceramika" value in "Polish (Poland)" for the "Mug material" attribute diff --git a/features/product/managing_products/adding_text_attribute_in_different_locales.feature b/features/product/managing_products/adding_text_attribute_in_different_locales.feature index 99faf39fc0..04aed9be9e 100644 --- a/features/product/managing_products/adding_text_attribute_in_different_locales.feature +++ b/features/product/managing_products/adding_text_attribute_in_different_locales.feature @@ -12,12 +12,11 @@ Feature: Adding text attributes in different locales to a product And the store has a text product attribute "Mug material" And I am logged in as an administrator - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Adding a product with a text attribute in different locales - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "mug" And I name it "PHP Mug" in "English (United States)" - And I set its price to "$100.00" for "Web" channel And I set its "Mug material" attribute to "Wood" in "English (United States)" And I set its "Mug material" attribute to "Drewno" in "Polish (Poland)" And I add it @@ -26,7 +25,7 @@ Feature: Adding text attributes in different locales to a product And attribute "Mug material" of product "PHP Mug" should be "Wood" in "English (United States)" And attribute "Mug material" of product "PHP Mug" should be "Drewno" in "Polish (Poland)" - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding a text attribute in different locales to an existing product When I want to modify the "Symfony Mug" product And I set its "Mug material" attribute to "Wood" in "English (United States)" diff --git a/features/product/managing_products/adding_text_attributes_to_existing_product.feature b/features/product/managing_products/adding_text_attributes_to_existing_product.feature index ca75e7f939..37e9a9f3a6 100644 --- a/features/product/managing_products/adding_text_attributes_to_existing_product.feature +++ b/features/product/managing_products/adding_text_attributes_to_existing_product.feature @@ -10,7 +10,7 @@ Feature: Adding attributes to an existing product And the store has a text product attribute "Overall length" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding a text attribute to an existing product When I want to modify the "44 Magnum" product And I set its "Overall length" attribute to "30.5 cm" in "English (United States)" @@ -18,7 +18,7 @@ Feature: Adding attributes to an existing product Then I should be notified that it has been successfully edited And attribute "Overall length" of product "44 Magnum" should be "30.5 cm" - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding another text attribute to an existing product Given this product has a text attribute "Gun caliber" with value "11 mm" in "English (United States)" locale When I want to modify the "44 Magnum" product @@ -28,7 +28,7 @@ Feature: Adding attributes to an existing product And attribute "Gun caliber" of product "44 Magnum" should be "11 mm" And attribute "Overall length" of product "44 Magnum" should be "30.5 cm" - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding and removing text attributes on product update page When I want to modify the "44 Magnum" product And I set its "Overall length" attribute to "30.5 cm" in "English (United States)" @@ -37,7 +37,7 @@ Feature: Adding attributes to an existing product Then I should be notified that it has been successfully edited And product "44 Magnum" should not have a "Overall length" attribute - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding and removing after saving text attributes on product update page Given this product has a text attribute "Gun caliber" with value "11 mm" in "English (United States)" locale When I want to modify the "44 Magnum" product diff --git a/features/product/managing_products/changing_associations_of_product.feature b/features/product/managing_products/changing_associations_of_product.feature index 40bbb4d3c8..546717b420 100644 --- a/features/product/managing_products/changing_associations_of_product.feature +++ b/features/product/managing_products/changing_associations_of_product.feature @@ -9,7 +9,7 @@ Feature: Changing associations of an existing product And the store has "LG G3", "LG headphones" and "LG earphones" products And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Changing associated products of a product association Given the product "LG G3" has an association "Accessories" with product "LG headphones" When I want to modify the "LG G3" product @@ -18,7 +18,19 @@ Feature: Changing associations of an existing product Then I should be notified that it has been successfully edited And this product should have an association "Accessories" with products "LG headphones" and "LG earphones" - @ui @mink:chromedriver + @api @no-ui + Scenario: Adding another product to a product association + Given the product "LG G3" has an association "Accessories" with product "LG headphones" + When I add the product "LG earphones" to this product association + Then this association should have products "LG headphones" and "LG earphones" + + @api @no-ui + Scenario: Changing a product of a product association + Given the product "LG G3" has an association "Accessories" with product "LG headphones" + When I change this product association's product to the "LG earphones" product + Then this association should only have product "LG earphones" + + @ui @mink:chromedriver @no-api Scenario: Removing an associated product of a product association Given the product "LG G3" has an association "Accessories" with products "LG headphones" and "LG earphones" When I want to modify the "LG G3" product @@ -27,3 +39,9 @@ Feature: Changing associations of an existing product Then I should be notified that it has been successfully edited And this product should have an association "Accessories" with product "LG headphones" And this product should not have an association "Accessories" with product "LG earphones" + + @api @no-ui + Scenario: Removing a product of a product association + Given the product "LG G3" has an association "Accessories" with products "LG headphones" and "LG earphones" + When I remove the product "LG earphones" from this product association + Then this association should only have product "LG headphones" diff --git a/features/product/managing_products/changing_images_of_product.feature b/features/product/managing_products/changing_images_of_product.feature index a5bcebd673..51579db369 100644 --- a/features/product/managing_products/changing_images_of_product.feature +++ b/features/product/managing_products/changing_images_of_product.feature @@ -5,7 +5,8 @@ Feature: Changing images of an existing product I want to be able to change images of an existing product Background: - Given I am logged in as an administrator + Given the store operates on a single channel in "United States" + And I am logged in as an administrator @ui @mink:chromedriver @no-api Scenario: Changing a single image of a simple product @@ -17,7 +18,7 @@ Feature: Changing images of an existing product Then I should be notified that it has been successfully edited And this product should have an image with "thumbnail" type - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Changing a single image of a configurable product Given the store has a "Lamborghini Gallardo Model" configurable product And this product has an image "ford.jpg" with "thumbnail" type @@ -27,26 +28,50 @@ Feature: Changing images of an existing product Then I should be notified that it has been successfully edited And this product should have an image with "thumbnail" type - @ui @javascript @no-api + @ui @javascript @api Scenario: Changing the type of image of a simple product - Given the store has a product "Lamborghini Ford Model" + Given the store has a product "Lamborghini Gallardo Model" And this product has an image "lamborghini.jpg" with "thumbnail" type And this product has an image "ford.jpg" with "banner" type When I want to modify this product And I change the first image type to "banner" - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this product should still have 2 images But it should not have any images with "thumbnail" type - @ui @javascript + @ui @javascript @api Scenario: Changing the type of image of a configurable product - Given the store has a "Lamborghini Ford Model" configurable product + Given the store has a "Lamborghini Gallardo Model" configurable product And this product has an image "lamborghini.jpg" with "thumbnail" type And this product has an image "ford.jpg" with "banner" type When I want to modify this product And I change the first image type to "banner" - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this product should still have 2 images But it should not have any images with "thumbnail" type + + @ui @mink:chromedriver @api + Scenario: Changing the variants of image of a configurable product + Given the store has a "Lamborghini Gallardo Model" configurable product + And this product has "Blue" and "Yellow" variants + And this product has an image "lamborghini.jpg" with "thumbnail" type + When I want to modify this product + And I select "Blue" variant for the first image + And I save my changes to the images + Then I should be notified that the changes have been successfully applied + And this product should still have only one image + And its image should have "Blue" variant selected + + @no-ui @api + Scenario: Changing the variants of image of a configurable product + Given the store has a "Porsche 911 Model" configurable product + And this product has "Red" and "Green" variants + And the store has a "Lamborghini Gallardo Model" configurable product + And this product has "Blue" and "Yellow" variants + And this product has an image "lamborghini.jpg" with "thumbnail" type + When I want to modify this product + And I select "Green" variant for the first image + And I save my changes to the images + Then I should be notified that the "Green" variant does not belong to this product diff --git a/features/product/managing_products/checking_products_in_the_shop_while_editing_them.feature b/features/product/managing_products/checking_products_in_the_shop_while_editing_them.feature index 274466777f..cb24c7c97d 100644 --- a/features/product/managing_products/checking_products_in_the_shop_while_editing_them.feature +++ b/features/product/managing_products/checking_products_in_the_shop_while_editing_them.feature @@ -10,7 +10,7 @@ Feature: Checking products in the shop while editing them And the store has a product "Bugatti" available in "United States" channel And I am logged in as an administrator - @ui + @ui @no-api Scenario: Accessing product show page in shop from the product edit page where product is available in more than one channel Given this product is available in the "Europe" channel And I am browsing products @@ -18,14 +18,14 @@ Feature: Checking products in the shop while editing them And I choose to show this product in the "Europe" channel Then I should see this product in the "Europe" channel in the shop - @ui + @ui @no-api Scenario: Accessing product show page in shop from the product edit page where product is available in one channel Given I am browsing products When I want to modify this product And I choose to show this product in this channel Then I should see this product in the "United States" channel in the shop - @ui + @ui @no-api Scenario: Being unable to access product show page in shop when the product is disabled Given this product has been disabled When I want to modify this product diff --git a/features/product/managing_products/deleting_multiple_products.feature b/features/product/managing_products/deleting_multiple_products.feature index 1a34c783b2..1b2bd5b4f1 100644 --- a/features/product/managing_products/deleting_multiple_products.feature +++ b/features/product/managing_products/deleting_multiple_products.feature @@ -8,7 +8,7 @@ Feature: Deleting multiple products Given the store has "Audi RS5 model", "Audi RS6 model" and "Audi RS7 model" products And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple products at once When I browse products And I check the "Audi RS5 model" product diff --git a/features/product/managing_products/editing_product.feature b/features/product/managing_products/editing_product.feature index 7b7979610f..53f1134270 100644 --- a/features/product/managing_products/editing_product.feature +++ b/features/product/managing_products/editing_product.feature @@ -10,7 +10,7 @@ Feature: Editing a product And I am logged in as an administrator @ui @api - Scenario: Seeing disabled code field when editing product + Scenario: Being unable to change code of an existing product When I want to modify the "Dice Brewing" product Then I should not be able to edit its code @@ -58,7 +58,7 @@ Feature: Editing a product Then I should be notified that it has been successfully edited And this product name should be "Sobieski Vodka" - @ui + @ui @api Scenario: Renaming a configurable product with option Given the store has a "Wyborowa Vodka" configurable product And the store has a product option "Bottle size" with a code "bottle_size" @@ -81,23 +81,24 @@ Feature: Editing a product Then I should be notified that it has been successfully edited And this product should have a "T-Shirt color" option - @ui - Scenario: Seeing disabled option field when editing product + @ui @api + Scenario: Being unable to change options of an existing product Given the store has a "Marvel's T-Shirt" configurable product And the store has a product option "T-Shirt size" with a code "t_shirt_size" And this product has this product option + And the store has also a product option "T-Shirt color" And the product "Marvel's T-Shirt" has "Iron Man T-Shirt" variant priced at "$40.00" When I want to modify the "Marvel's T-Shirt" product - Then the option field should be disabled + Then I should not be able to edit its options - @ui + @ui @api Scenario: Enabling product in channel when all its variants already have specified price in this channel Given the store operates on another channel named "Mobile" And the store has a "7 Wonders" configurable product - And this product has "7 Wonders: Cities" variant priced at "$30" in "United States" channel - And this variant is also priced at "$25" in "Mobile" channel - And this product has "7 Wonders: Leaders" variant priced at "$20" in "United States" channel - And this variant is also priced at "$20" in "Mobile" channel + And this product has "7 Wonders: Cities" variant priced at "$30.00" in "United States" channel + And this variant is also priced at "$25.00" in "Mobile" channel + And this product has "7 Wonders: Leaders" variant priced at "$20.00" in "United States" channel + And this variant is also priced at "$20.00" in "Mobile" channel When I want to modify the "7 Wonders" product And I assign it to channel "Mobile" And I save my changes diff --git a/features/product/managing_products/editing_product_attributes.feature b/features/product/managing_products/editing_product_attributes.feature index 38a2b598e4..3d8ca4fc5c 100644 --- a/features/product/managing_products/editing_product_attributes.feature +++ b/features/product/managing_products/editing_product_attributes.feature @@ -11,7 +11,7 @@ Feature: Editing product's attributes And this product has a text attribute "Gun caliber" with value "11 mm" And I am logged in as an administrator - @ui @javascript + @ui @javascript @no-api Scenario: Seeing message about no new attributes selected When I want to modify the "44 Magnum" product And I try to add new attributes @@ -19,7 +19,7 @@ Feature: Editing product's attributes Then attribute "Gun caliber" of product "44 Magnum" should be "11 mm" And product "44 Magnum" should have 1 attribute - @ui @javascript + @ui @javascript @no-api Scenario: Seeing message about no new attributes selected after all attributes deletion When I want to modify the "44 Magnum" product And I remove its "Gun caliber" attribute diff --git a/features/product/managing_products/editing_product_slug.feature b/features/product/managing_products/editing_product_slug.feature index bed0aa16af..f34a603f8f 100644 --- a/features/product/managing_products/editing_product_slug.feature +++ b/features/product/managing_products/editing_product_slug.feature @@ -25,7 +25,7 @@ Feature: Editing product's slug And I add it Then the slug of the "Mansion of Madness" product should be "mom-board-game" - @ui + @ui @no-api Scenario: Seeing disabled slug field when editing a product Given the store has a product "Mansion of Madness" When I want to modify this product diff --git a/features/product/managing_products/editing_product_slug_in_multiple_locales.feature b/features/product/managing_products/editing_product_slug_in_multiple_locales.feature index b25d701c2d..40b0cc6b22 100644 --- a/features/product/managing_products/editing_product_slug_in_multiple_locales.feature +++ b/features/product/managing_products/editing_product_slug_in_multiple_locales.feature @@ -10,12 +10,10 @@ Feature: Editing product's slug in multiple locales And the store is also available in "Polish (Poland)" And I am logged in as an administrator - @ui @no-api + @ui @api Scenario: Creating a product with custom slugs - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "PUG_PUGGINTON_PLUSHIE" - And I set its price to "$100.00" for "United States" channel - And I set its price to "$100.00" for "United States" channel And I name it "Pug Pugginton Plushie" in "English (United States)" And I set its slug to "sir-pugginton" in "English (United States)" And I name it "Pluszak Mops Mopsiński" in "Polish (Poland)" @@ -24,35 +22,34 @@ Feature: Editing product's slug in multiple locales Then the slug of the "Pug Pugginton Plushie" product should be "sir-pugginton" in the "English (United States)" locale And the slug of the "Pug Pugginton Plushie" product should be "pan-mopsinski" in the "Polish (Poland)" locale - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Creating a product with autogenerated slugs - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "PUG_PUGGINTON_PLUSHIE" - And I set its price to "$100.00" for "United States" channel And I name it "Pug Pugginton Plushie" in "English (United States)" And I name it "Pluszak Mops Mopsiński" in "Polish (Poland)" And I add it Then the slug of the "Pug Pugginton Plushie" product should be "pug-pugginton-plushie" in the "English (United States)" locale And the slug of the "Pug Pugginton Plushie" product should be "pluszak-mops-mopsinski" in the "Polish (Poland)" locale - @ui + @ui @no-api Scenario: Seeing disabled slug fields when editing a product Given the store has a product named "Pug Pugginton Plushie" in "English (United States)" locale and "Pluszak Mops Mopsiński" in "Polish (Poland)" locale When I want to modify this product Then the slug field in "English (United States)" should not be editable And the slug field in "Polish (Poland)" also should not be editable - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Slugs don't get updated while changing product's names Given the store has a product named "Pug Pugginton Plushie" in "English (United States)" locale and "Pluszak Mops Mopsiński" in "Polish (Poland)" locale When I want to modify this product And I rename it to "Pug Pugston the Third Plushie" in "English (United States)" And I rename it to "Pluszak Mops Mopsak Trzeci" in "Polish (Poland)" And I save my changes - Then this product should still have slug "pug-pugginton-plushie" in "English (United States)" - And this product should still have slug "pluszak-mops-mopsinski" in "Polish (Poland)" + Then this product should still have slug "pug-pugginton-plushie" in "English (United States)" locale + And this product should still have slug "pluszak-mops-mopsinski" in "Polish (Poland)" locale - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Enabling automatic slugs update on product's names change Given the store has a product named "Pug Pugginton Plushie" in "English (United States)" locale and "Pluszak Mops Mopsiński" in "Polish (Poland)" locale When I want to modify this product @@ -61,10 +58,10 @@ Feature: Editing product's slug in multiple locales And I enable slug modification in "Polish (Poland)" And I rename it to "Pluszak Mops Mopsak Trzeci" in "Polish (Poland)" And I save my changes - Then this product should have slug "pug-pugston-the-third-plushie" in "English (United States)" - And this product should have slug "pluszak-mops-mopsak-trzeci" in "Polish (Poland)" + Then this product should have slug "pug-pugston-the-third-plushie" in "English (United States)" locale + And this product should have slug "pluszak-mops-mopsak-trzeci" in "Polish (Poland)" locale - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Manually modifying slugs on product's names change Given the store has a product named "Pug Pugginton Plushie" in "English (United States)" locale and "Pluszak Mops Mopsiński" in "Polish (Poland)" locale When I want to modify this product @@ -75,5 +72,5 @@ Feature: Editing product's slug in multiple locales And I rename it to "Pluszak Mops Mopsak Trzeci" in "Polish (Poland)" And I set its slug to "pan-mopsak-trzeci" in "Polish (Poland)" And I save my changes - Then this product should have slug "sir-pugston-the-third" in "English (United States)" - And this product should have slug "pan-mopsak-trzeci" in "Polish (Poland)" + Then this product should have slug "sir-pugston-the-third" in "English (United States)" locale + And this product should have slug "pan-mopsak-trzeci" in "Polish (Poland)" locale diff --git a/features/product/managing_products/filtering_products_by_channel.feature b/features/product/managing_products/filtering_products_by_channel.feature index 52afb64b14..990001a747 100644 --- a/features/product/managing_products/filtering_products_by_channel.feature +++ b/features/product/managing_products/filtering_products_by_channel.feature @@ -12,7 +12,7 @@ Feature: Filtering products by a channel And the store also has a product "HP Spectre" in channel "Web-US" And I am logged in as an administrator - @ui + @ui @api Scenario: Filtering products by a chosen channel When I browse products And I choose "Web-EU" as a channel filter @@ -22,7 +22,7 @@ Feature: Filtering products by a channel And I should see a product with name "MacBook Pro" But I should not see any product with name "HP Spectre" - @ui + @ui @api Scenario: Filtering products by a chosen channel When I browse products And I choose "Web-US" as a channel filter diff --git a/features/product/managing_products/modifying_taxons_assigned_to_an_existing_product.feature b/features/product/managing_products/modifying_taxons_assigned_to_an_existing_product.feature new file mode 100644 index 0000000000..cf0f4fc3a4 --- /dev/null +++ b/features/product/managing_products/modifying_taxons_assigned_to_an_existing_product.feature @@ -0,0 +1,45 @@ +@managing_products +Feature: Modifying taxons assigned to an existing product + In order to specify in which taxon a product is available + As an Administrator + I want to be able to change taxon of a product + + Background: + Given the store operates on a single channel in "United States" + And the store classifies its products as "Clothes" and "T-Shirts" + And the store has a "Shirt" configurable product + And the store has a "T-Shirt" configurable product + And the product "T-Shirt" belongs to taxon "Clothes" + And I am logged in as an administrator + + @ui @api + Scenario: Modifying taxons assigned to a product + When I change that the "T-Shirt" product does not belong to the "Clothes" taxon + And I add "T-Shirts" taxon to the "T-Shirt" product + Then the product "T-Shirt" should have the "T-Shirts" taxon + + @ui @api + Scenario: Adding taxons to product + When I add "Clothes" taxon to the "Shirt" product + Then the product "Shirt" should have the "Clothes" taxon + + @api @no-ui + Scenario: Being prevented from adding the same taxon twice + When I try to add "Clothes" taxon to the "T-Shirt" product + Then I should be notified that product taxons cannot be duplicated + + @api @no-ui + Scenario: Being prevented from assigning an empty taxon to a product + When I try to assign an empty taxon to the "T-Shirt" product + Then I should be notified that specifying a taxon is required + + @api @no-ui + Scenario: Being prevented from assigning an empty product to a taxon + When I try to assign an empty product to the "Clothes" taxon + Then I should be notified that specifying a product is required + + @api @no-ui + Scenario: Being unable duplicate a product taxon by getting it from another product + Given the product "Shirt" belongs to taxon "Clothes" + When I try to assign the product taxon of product "T-Shirt" and taxon "Clothes" to the product "Shirt" + Then I should be notified that product taxons cannot be duplicated diff --git a/features/product/managing_products/prevent_deletion_of_purchased_product.feature b/features/product/managing_products/preventing_deletion_of_purchased_product.feature similarity index 96% rename from features/product/managing_products/prevent_deletion_of_purchased_product.feature rename to features/product/managing_products/preventing_deletion_of_purchased_product.feature index 2d7d1bddad..a55089cd88 100644 --- a/features/product/managing_products/prevent_deletion_of_purchased_product.feature +++ b/features/product/managing_products/preventing_deletion_of_purchased_product.feature @@ -1,5 +1,5 @@ @managing_products -Feature: Prevent deletion of purchased product +Feature: Preventing deletion of purchased product In order to maintain proper order history As an Administrator I want to be prevented from deleting purchased products @@ -14,7 +14,7 @@ Feature: Prevent deletion of purchased product And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @domain @ui + @domain @ui @api Scenario: Purchased product cannot be deleted When I try to delete the "Toyota GT86 model" product Then I should be notified that this product is in use and cannot be deleted diff --git a/features/product/managing_products/product_validation.feature b/features/product/managing_products/product_validation.feature index 34d81cda6d..89d9a91c79 100644 --- a/features/product/managing_products/product_validation.feature +++ b/features/product/managing_products/product_validation.feature @@ -36,7 +36,7 @@ Feature: Products validation @ui @no-api Scenario: Adding a new simple product with duplicated code among product variants Given the store has a product "7 Wonders" - And this product has "7 Wonders: Cities" variant priced at "$30" identified by "AWESOME_GAME" + And this product has "7 Wonders: Cities" variant priced at "$30.00" identified by "AWESOME_GAME" When I want to create a new simple product And I specify its code as "AWESOME_GAME" And I name it "Dice Brewing" in "English (United States)" @@ -131,60 +131,50 @@ Feature: Products validation And this product should still be named "Dice Brewing" @ui @api - Scenario: Not seeing validation error for duplicated code if product code has not been changed - Given the store has a "Dice Brewing" product - When I want to modify this product - And I remove its name from "English (United States)" translation - And I try to save my changes - Then this product should still be named "Dice Brewing" - - @ui Scenario: Trying to assign new channel to an existing configurable product without specifying its all variant prices for this channel Given the store has a "7 Wonders" configurable product - And this product has "7 Wonders: Cities" variant priced at "$30" - And this product has "7 Wonders: Leaders" variant priced at "$20" + And this product has "7 Wonders: Cities" variant priced at "$30.00" + And this product has "7 Wonders: Leaders" variant priced at "$20.00" And the store operates on another channel named "Mobile Channel" When I want to modify the "7 Wonders" product And I assign it to channel "Mobile Channel" And I save my changes Then I should be notified that I have to define product variants' prices for newly assigned channels first - @ui @no-api - Scenario: Adding a new simple product with price + @ui @api + Scenario: Adding a new product with duplicated slug Given the store has a "7 Wonders" configurable product with "7-wonders" slug When I want to create a new configurable product And I specify its code as "7-WONDERS-BABEL" And I name it "7 Wonders Babel" in "English (United States)" And I set its slug to "7-wonders" in "English (United States)" - And I add it + And I try to add it Then I should be notified that slug has to be unique And product with code "7-WONDERS-BABEL" should not be added - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Trying to add a new product with a text attribute without specifying its value in default locale - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "X-18-MUG" And I name it "PHP Mug" in "English (United States)" - And I set its price to "$100.00" for "Web" channel And I set its "Mug material" attribute to "Drewno" in "Polish (Poland)" But I do not set its "Mug material" attribute in "English (United States)" And I add it Then I should be notified that I have to define the "Mug material" attribute in "English (United States)" And product with code "X-18-MUG" should not be added - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Trying to add a new product with a text attribute without specifying its value in additional locale with proper length - When I want to create a new simple product + When I want to create a new configurable product And I specify its code as "X-18-MUG" And I name it "PHP Mug" in "English (United States)" - And I set its price to "$100.00" for "Web" channel And I set its "Mug material" attribute to "Dr" in "Polish (Poland)" And I set its "Mug material" attribute to "Wood" in "English (United States)" And I add it Then I should be notified that the "Mug material" attribute in "Polish (Poland)" should be longer than 3 And product with code "X-18-MUG" should not be added - @ui @javascript + @ui @javascript @api Scenario: Trying to add a text attribute in different locales to an existing product without specifying its value in default locale When I want to modify the "Symfony Mug" product And I set its "Mug material" attribute to "Drewno" in "Polish (Poland)" @@ -192,7 +182,7 @@ Feature: Products validation And I save my changes Then I should be notified that I have to define the "Mug material" attribute in "English (United States)" - @ui @javascript + @ui @javascript @api Scenario: Trying to add a text attribute in different locales to an existing product without specifying its value in additional locale with proper length When I want to modify the "Symfony Mug" product And I set its "Mug material" attribute to "Dr" in "Polish (Poland)" diff --git a/features/product/managing_products/removing_images_of_product.feature b/features/product/managing_products/removing_images_of_product.feature index ada168cef1..3f9aa42f14 100644 --- a/features/product/managing_products/removing_images_of_product.feature +++ b/features/product/managing_products/removing_images_of_product.feature @@ -7,27 +7,27 @@ Feature: Removing images of an existing product Background: Given I am logged in as an administrator - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Removing a single image of a simple product Given the store has a product "Lamborghini Gallardo Model" And this product has an image "lamborghini.jpg" with "thumbnail" type When I want to modify this product And I remove an image with "thumbnail" type - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this product should not have any images - @ui @javascript + @ui @javascript @api Scenario: Removing a single image of a configurable product Given the store has a "Lamborghini Gallardo Model" configurable product And this product has an image "lamborghini.jpg" with "thumbnail" type When I want to modify this product And I remove an image with "thumbnail" type - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this product should not have any images - @ui @javascript @no-api + @ui @javascript @api Scenario: Removing all images of a simple product Given the store has a product "Lamborghini Gallardo Model" And this product has an image "lamborghini.jpg" with "thumbnail" type @@ -35,11 +35,11 @@ Feature: Removing images of an existing product When I want to modify this product And I remove an image with "thumbnail" type And I also remove an image with "main" type - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this product should not have any images - @ui @javascript + @ui @javascript @api Scenario: Removing all images of a configurable product Given the store has a "Lamborghini Gallardo Model" configurable product And this product has an image "lamborghini.jpg" with "thumbnail" type @@ -47,42 +47,42 @@ Feature: Removing images of an existing product When I want to modify this product And I remove an image with "thumbnail" type And I also remove an image with "main" type - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this product should not have any images - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Removing only one image of a simple product Given the store has a product "Lamborghini Gallardo Model" And this product has an image "lamborghini.jpg" with "thumbnail" type And it also has an image "lamborghini.jpg" with "main" type When I want to modify this product And I remove an image with "thumbnail" type - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this product should have an image with "main" type But it should not have any images with "thumbnail" type - @ui @mink:chromedriver @no-api + @ui @mink:chromedriver @api Scenario: Removing only one image of a simple product when all images have same type Given the store has a product "Lamborghini Ford Model" And this product has an image "lamborghini.jpg" with "thumbnail" type And it also has an image "ford.jpg" with "thumbnail" type When I want to modify this product And I remove the first image - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this product should have only one image - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Removing only one image of a configurable product Given the store has a "Lamborghini Gallardo Model" configurable product And this product has an image "lamborghini.jpg" with "thumbnail" type And it also has an image "lamborghini.jpg" with "main" type When I want to modify this product And I remove an image with "thumbnail" type - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this product should have an image with "main" type But it should not have any images with "thumbnail" type @@ -99,7 +99,7 @@ Feature: Removing images of an existing product And this product should have an image with "main" type But it should not have any images with "thumbnail" type - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Adding multiple images and removing a single image of a configurable product Given the store has a "Lamborghini Gallardo Model" configurable product When I want to modify this product diff --git a/features/product/managing_products/removing_product_price_after_channel_deletion.feature b/features/product/managing_products/removing_product_price_after_channel_deletion.feature index 88fbe4771e..63569065b2 100644 --- a/features/product/managing_products/removing_product_price_after_channel_deletion.feature +++ b/features/product/managing_products/removing_product_price_after_channel_deletion.feature @@ -12,7 +12,7 @@ Feature: Removing a product's price after channel deletion And this product is also priced at "£5.00" in "Web-GB" channel And I am logged in as an administrator - @ui + @ui @no-api Scenario: Removing a product's price after corresponding channel deletion When channel "Web-GB" has been deleted Then product "Dice Brewing" should be priced at $10.00 for channel "Web-US" diff --git a/features/product/managing_products/removing_product_price_from_obsolete_channel.feature b/features/product/managing_products/removing_product_price_from_obsolete_channel.feature index 34333a83cd..baa7681125 100644 --- a/features/product/managing_products/removing_product_price_from_obsolete_channel.feature +++ b/features/product/managing_products/removing_product_price_from_obsolete_channel.feature @@ -13,20 +13,20 @@ Feature: Removing a product's price from the channel where it is not available i And this product is disabled in "Web-GB" channel And I am logged in as an administrator - @ui + @ui @no-api Scenario: Removing a product's price from disabled channel Given the channel "Web-GB" has been disabled When I want to modify the "Dice Brewing" product - And I remove its price for "Web-GB" channel + And I remove its price from "Web-GB" channel And I save my changes Then I should not have configured price for "Web-GB" channel But I should have original price equal to "£70.00" in "Web-GB" channel - @ui + @ui @no-api Scenario: Removing a product's price from obsolete channel Given this product is disabled in "Web-GB" channel When I want to modify the "Dice Brewing" product - And I remove its price for "Web-GB" channel + And I remove its price from "Web-GB" channel And I save my changes Then I should not have configured price for "Web-GB" channel But I should have original price equal to "£70.00" in "Web-GB" channel diff --git a/features/product/managing_products/sorting_products.feature b/features/product/managing_products/sorting_products.feature index d21e4322c3..6e1b076594 100644 --- a/features/product/managing_products/sorting_products.feature +++ b/features/product/managing_products/sorting_products.feature @@ -65,7 +65,7 @@ Feature: Sorting listed products Then I should see 3 products in the list And the first product on the list should have name "Szałowy Mops" - @ui @no-postgres + @ui @api @no-postgres Scenario: Missing translations are sorted as first when sorting by name ascending When I change my locale to "Polish" And I browse products @@ -74,7 +74,7 @@ Feature: Sorting listed products And the first product on the list shouldn't have a name And the last product on the list should have name "Ekstremalny Mops" - @ui @no-postgres + @ui @api @no-postgres Scenario: Missing translation are sorted as last when sorting by name descending When I change my locale to "Polish" And I browse products diff --git a/features/product/managing_products/sorting_products_within_a_taxon_by_position.feature b/features/product/managing_products/sorting_products_within_a_taxon_by_position.feature index 53079da61b..0e9c3496b2 100644 --- a/features/product/managing_products/sorting_products_within_a_taxon_by_position.feature +++ b/features/product/managing_products/sorting_products_within_a_taxon_by_position.feature @@ -5,7 +5,8 @@ Feature: Sorting listed products from a taxon by position I want to sort products from a taxon by their positions Background: - Given the store classifies its products as "Soft Toys" + Given the store operates on a single channel in "United States" + And the store classifies its products as "Soft Toys" And the store has a product "Old pug" in the "Soft Toys" taxon at 0th position And the store has a product "Young pug" in the "Soft Toys" taxon at 1st position And the store has a product "Small pug" in the "Soft Toys" taxon at 2nd position @@ -28,7 +29,7 @@ Feature: Sorting listed products from a taxon by position And the store has a product "Ultimate Pug" in the "Soft Toys" taxon at 19th position And I am logged in as an administrator - @ui + @ui @no-api Scenario: Setting two products to position -1 on the non-last page When I am browsing the 1st page of products from "Soft Toys" taxon And I set the position of "Old pug" to "-1" @@ -38,7 +39,7 @@ Feature: Sorting listed products from a taxon by position Then the one before last product on the list should have name "Old pug" with position 18 And the last product on the list should have name "Young pug" with position 19 - @ui + @ui @no-api Scenario: Setting two products to position -1 on the last page When I am browsing the 2nd page of products from "Soft Toys" taxon And I set the position of "Pug XL" to "-1" @@ -47,7 +48,7 @@ Feature: Sorting listed products from a taxon by position Then the one before last product on the list should have name "Pug XL" with position 18 And the last product on the list should have name "Pug XS" with position 19 - @ui + @ui @no-api Scenario: Setting two products to the already occupied position on the other page When I am browsing the 1st page of products from "Soft Toys" taxon And I set the position of "Old pug" to "15" @@ -63,7 +64,7 @@ Feature: Sorting listed products from a taxon by position And the 7th product on this page should be named "Pug Master" And this product should be at position 16 - @ui + @ui @no-api Scenario: Setting two products to the already occupied position on the same page When I am browsing the 2nd page of products from "Soft Toys" taxon And I set the position of "Puglet" to "15" @@ -79,7 +80,7 @@ Feature: Sorting listed products from a taxon by position And the 7th product on this page should be named "Pug Master" And this product should be at position 16 - @ui + @ui @no-api Scenario: Setting two products to the positions overflowing the max available position on the non-last page When I am browsing the 1st page of products from "Soft Toys" taxon And I set the position of "Old pug" to "25" @@ -89,7 +90,7 @@ Feature: Sorting listed products from a taxon by position Then the one before last product on the list should have name "Old pug" with position 18 And the last product on the list should have name "Young pug" with position 19 - @ui + @ui @no-api Scenario: Setting two products to the positions overflowing the max available position on the last page When I am browsing the 2nd page of products from "Soft Toys" taxon And I set the position of "Puglet" to "25" @@ -99,33 +100,35 @@ Feature: Sorting listed products from a taxon by position Then the one before last product on the list should have name "Puglet" with position 18 And the last product on the list should have name "Pug XL" with position 19 - @ui + @ui @api Scenario: New product is added as last one Given I added a product "Big pug" And I assigned this product to "Soft Toys" taxon When I am browsing the 3rd page of products from "Soft Toys" taxon - Then the last product on the list should have name "Big pug" + Then the last product on the list within this taxon should have name "Big pug" - @ui + @ui @api Scenario: Product with position 0 is set as the first one When I am browsing products from "Soft Toys" taxon And I set the position of "Young pug" to 0 And I save my new configuration - Then the first product on the list should have name "Young pug" + And I go to the 1st page of products from "Soft Toys" taxon + Then the first product on the list within this taxon should have name "Young pug" - @ui + @ui @api Scenario: Being unable to use a non-numeric string as a product position Given I am browsing products from "Soft Toys" taxon When I set the position of "Young pug" to "test" And I save my new configuration Then I should be notified that the position "test" is invalid - @ui + @ui @api Scenario: Sort products in descending order When I am browsing products from "Soft Toys" taxon - And I start sorting products by position - Then the first product on the list should have name "Ultimate Pug" - @ui + And I sort this taxon's products "descending" by "position" + Then the first product on the list within this taxon should have name "Ultimate Pug" + + @ui @api Scenario: Products are sorted by position in ascending order by default When I am browsing products from "Soft Toys" taxon - Then the first product on the list should have name "Old pug" + Then the first product on the list within this taxon should have name "Old pug" diff --git a/features/product/managing_products/disable_or_enable_inventory_on_product.feature b/features/product/managing_products/toggling_inventory_on_product.feature similarity index 96% rename from features/product/managing_products/disable_or_enable_inventory_on_product.feature rename to features/product/managing_products/toggling_inventory_on_product.feature index 352b9c91ba..79d54d924d 100644 --- a/features/product/managing_products/disable_or_enable_inventory_on_product.feature +++ b/features/product/managing_products/toggling_inventory_on_product.feature @@ -1,5 +1,5 @@ @managing_products -Feature: Toggle the inventory tracking +Feature: Toggling the inventory tracking In order to have the inventory tracked in my shop As an Administrator I want to toggle the inventory tracking diff --git a/features/product/managing_products/disable_or_enable_product.feature b/features/product/managing_products/toggling_product.feature similarity index 96% rename from features/product/managing_products/disable_or_enable_product.feature rename to features/product/managing_products/toggling_product.feature index daa1abc35c..cc8bd11389 100644 --- a/features/product/managing_products/disable_or_enable_product.feature +++ b/features/product/managing_products/toggling_product.feature @@ -1,5 +1,5 @@ @managing_products -Feature: Toggle the product +Feature: Toggling the simple product In order to stop or resume the sale of a product As an Administrator I want to toggle the product diff --git a/features/product/managing_products/viewing_non_translatable_attributes.feature b/features/product/managing_products/viewing_non_translatable_attributes.feature index 8f8ed3a714..5a5387fcb9 100644 --- a/features/product/managing_products/viewing_non_translatable_attributes.feature +++ b/features/product/managing_products/viewing_non_translatable_attributes.feature @@ -10,7 +10,7 @@ Feature: Viewing product's non translatable attributes on edit page And this product has non-translatable percent attribute "crit chance" with value 10% And I am logged in as an administrator - @ui + @ui @api Scenario: Viewing product's attributes defined in different locales When I modify the "Iron Pickaxe" product - And I should see non-translatable attribute "crit chance" with value "10" + And I should see non-translatable attribute "crit chance" with value 10% diff --git a/features/product/viewing_price_history/accessing_price_history_from_configurable_product.feature b/features/product/viewing_price_history/accessing_price_history_from_configurable_product.feature new file mode 100644 index 0000000000..e924dce57b --- /dev/null +++ b/features/product/viewing_price_history/accessing_price_history_from_configurable_product.feature @@ -0,0 +1,19 @@ +@accessing_price_history +Feature: Accessing the price history from the configurable product show page + In order to check the price history of an product variant + As an Administrator + I want to be able to access the price history's page for the given channel from a configurable product show page + + Background: + Given the store operates on a single channel in "United States" + And the store has a "Wyborowa Vodka" configurable product + And the product "Wyborowa Vodka" has a "Wyborowa Vodka Exquisite" variant priced at "$40.00" and originally priced at "$15.00" + And the product "Wyborowa Vodka" has a "Wyborowa Vodka Lemon" variant priced at "$10.00" + And I am logged in as an administrator + + @ui @no-api + Scenario: Being able to access the price history of variant from the configurable product show page + Given I am browsing products + When I access the "Wyborowa Vodka" product + And I access the price history of a product variant "Wyborowa Vodka Exquisite" for "United States" channel + Then I should see 1 log entries in the catalog price history diff --git a/features/product/viewing_price_history/accessing_price_history_from_simple_product.feature b/features/product/viewing_price_history/accessing_price_history_from_simple_product.feature new file mode 100644 index 0000000000..97325e1088 --- /dev/null +++ b/features/product/viewing_price_history/accessing_price_history_from_simple_product.feature @@ -0,0 +1,22 @@ +@accessing_price_history +Feature: Accessing the price history from the simple product show page + In order to check the price history of a simple product + As an Administrator + I want to be able to access the price history's page for the given channel from a simple product show page + + Background: + Given the store operates on a single channel in "United States" + And the store has a product "Ursus C-355" priced at "$1,000.00" in "United States" channel + And there is a catalog promotion with "company_bankruptcy_sale" code and "Company bankruptcy sale" name + And the catalog promotion "Company bankruptcy sale" is available in "United States" + And it applies on "Ursus C-355" product + And it reduces price by "90%" + And it is enabled + And I am logged in as an administrator + + @ui @no-api + Scenario: Being able to access price history from simple product show page + Given I am browsing products + When I access the "Ursus C-355" product + And I access the price history of a simple product for "United States" channel + Then I should see 2 log entries in the catalog price history diff --git a/features/product/viewing_price_history/seeing_price_history_after_creating_product_variant.feature b/features/product/viewing_price_history/seeing_price_history_after_creating_product_variant.feature new file mode 100644 index 0000000000..d17de228b8 --- /dev/null +++ b/features/product/viewing_price_history/seeing_price_history_after_creating_product_variant.feature @@ -0,0 +1,45 @@ +@viewing_price_history +Feature: Seeing the correct catalog price history after creating a product variant + In order to be aware of historical product prices + As an Administrator + I want to see the catalog price history of the product I've created + + Background: + Given the store operates on a single channel in "United States" + And I am logged in as an administrator + + @api @ui + Scenario: Seeing historical product variant prices after the product variant has been created without any promotion applied + Given the store has a "Wyborowa Vodka" configurable product + When I want to create a new variant of this product + And I specify its code as "WYBOROWA_VODKA" + And I set its price to "$20.00" for "United States" channel + And I set its original price to "$25.00" for "United States" channel + And I add it + And I go to the price history of a variant with code "WYBOROWA_VODKA" + Then I should see a single log entry in the catalog price history + And there should be a log entry with the "$20.00" selling price, "$25.00" original price and datetime of the price change + + @api @ui + Scenario: Seeing historical product variant prices after the product variant has been created without original price and any promotion applied + Given the store has a "Wyborowa Vodka" configurable product + When I want to create a new variant of this product + And I specify its code as "WYBOROWA_VODKA" + And I set its price to "$20.00" for "United States" channel + And I add it + And I go to the price history of a variant with code "WYBOROWA_VODKA" + Then I should see a single log entry in the catalog price history + And there should be a log entry with the "$20.00" selling price, no original price and datetime of the price change + + @api @ui + Scenario: Seeing historical product variant prices after the product variant has been created with catalog promotions applied + Given the store has a "Wyborowa Vodka" configurable product + And there is a catalog promotion "Christmas sale" that reduces price by "50%" and applies on "Wyborowa Vodka" product + When I want to create a new variant of this product + And I specify its code as "WYBOROWA_VODKA" + And I set its price to "$20.00" for "United States" channel + And I add it + And I go to the price history of a variant with code "WYBOROWA_VODKA" + Then I should see 2 log entries in the catalog price history + And there should be a log entry on the 1st position with the "$10.00" selling price, "$20.00" original price and datetime of the price change + And there should be a log entry on the 2nd position with the "$20.00" selling price, no original price and datetime of the price change diff --git a/features/product/viewing_price_history/seeing_price_history_after_editing_product_variant.feature b/features/product/viewing_price_history/seeing_price_history_after_editing_product_variant.feature new file mode 100644 index 0000000000..132773200f --- /dev/null +++ b/features/product/viewing_price_history/seeing_price_history_after_editing_product_variant.feature @@ -0,0 +1,20 @@ +@viewing_price_history +Feature: Seeing the correct catalog price history after editing a product variant + In order to be aware of historical product prices + As an Administrator + I want to see the catalog price history of the product I've changed + + Background: + Given the store operates on a single channel in "United States" + And the store has a product "Wyborowa Vodka" priced at "$40.00" in "United States" channel + And I am logged in as an administrator + + @api @ui + Scenario: Seeing historical product variant prices after the product variant has been edited + When I want to modify the "Wyborowa Vodka" product variant + And I change its price to "$42.00" for "United States" channel + And I save my changes + And I go to the price history of a variant with code "WYBOROWA_VODKA" + Then I should see 2 log entries in the catalog price history + And there should be a log entry on the 1st position with the "$42.00" selling price, no original price and datetime of the price change + And there should be a log entry on the 2nd position with the "$40.00" selling price, no original price and datetime of the price change diff --git a/features/product/viewing_price_history/seeing_price_history_of_product_variant_after_changes_to_catalog_promotions.feature b/features/product/viewing_price_history/seeing_price_history_of_product_variant_after_changes_to_catalog_promotions.feature new file mode 100644 index 0000000000..79674a36ba --- /dev/null +++ b/features/product/viewing_price_history/seeing_price_history_of_product_variant_after_changes_to_catalog_promotions.feature @@ -0,0 +1,29 @@ +@viewing_price_history_after_catalog_promotions +Feature: Seeing the price history of a product variant after changes to catalog promotions + In order to be aware of historical variant prices + As an Administrator + I want to browse the catalog price history of a specific variant + + Background: + Given the store operates on a single channel in "United States" + And the store has a "Wyborowa Vodka" configurable product + And there is a catalog promotion "Winter sale" with priority 1 that reduces price by "50%" and applies on "Wyborowa Vodka" product + And there is a catalog promotion "Christmas sale" with priority 2 that reduces price by fixed "$5.00" in the "United States" channel and applies on "Wyborowa Vodka" product + And the product "Wyborowa Vodka" has a "Wyborowa Vodka Exquisite" variant priced at "$40.00" and originally priced at "$15.00" + And the product "Wyborowa Vodka" has a "Wyborowa Vodka Lemon" variant priced at "$10.00" + And there is an exclusive catalog promotion "Extra sale" with priority 10 that reduces price by "10%" and applies on "Wyborowa Vodka Lemon" variant + And I am logged in as an administrator + + @api @ui + Scenario: Seeing the catalog price history of a variant with many catalog promotions + And I go to the price history of a variant with code "WYBOROWA_VODKA_EXQUISITE" + Then I should see 2 log entries in the catalog price history + And there should be a log entry on the 1st position with the "$5.00" selling price, "$15.00" original price and datetime of the price change + And there should be a log entry on the 2nd position with the "$40.00" selling price, "$15.00" original price and datetime of the price change + + @api @ui + Scenario: Seeing the catalog price history of a variant with one catalog promotion + And I go to the price history of a variant with code "WYBOROWA_VODKA_LEMON" + Then I should see 2 log entries in the catalog price history + And there should be a log entry on the 1st position with the "$9.00" selling price, "$10.00" original price and datetime of the price change + And there should be a log entry on the 2nd position with the "$10.00" selling price, no original price and datetime of the price change diff --git a/features/product/viewing_price_history/seeing_price_history_of_simple_product_after_changes_to_catalog_promotions.feature b/features/product/viewing_price_history/seeing_price_history_of_simple_product_after_changes_to_catalog_promotions.feature new file mode 100644 index 0000000000..a13185f909 --- /dev/null +++ b/features/product/viewing_price_history/seeing_price_history_of_simple_product_after_changes_to_catalog_promotions.feature @@ -0,0 +1,39 @@ +@viewing_price_history_after_catalog_promotions +Feature: Seeing the price history of a simple product after changes to catalog promotions + In order to be aware of historical simple product prices + As an Administrator + I want to browse the catalog price history of a simple product + + Background: + Given the store operates on a single channel in "United States" + And the store has a product "Wyborowa Vodka" priced at "$100.00" in "United States" channel + And there is disabled catalog promotion named "Winter sale" + And the catalog promotion "Winter sale" is available in "United States" + And it applies on "Wyborowa Vodka" product + And it reduces price by "90%" + And I am logged in as an administrator + + @api @ui + Scenario: Seeing the catalog price history of a simple product + Given the "Winter sale" catalog promotion is enabled + When I disable "Winter sale" catalog promotion + And I go to the price history of a variant with code "WYBOROWA_VODKA" + Then I should see 3 log entries in the catalog price history + And there should be a log entry on the 1st position with the "$100.00" selling price, "$100.00" original price and datetime of the price change + And there should be a log entry on the 2nd position with the "$10.00" selling price, "$100.00" original price and datetime of the price change + And there should be a log entry on the 3rd position with the "$100.00" selling price, no original price and datetime of the price change + + @api @ui + Scenario: Seeing the catalog price history of a simple product with a price different than an original price + Given "Wyborowa Vodka" variant is originally priced at "$120.00" in "United States" channel + And the "Winter sale" catalog promotion is enabled + And the "Wyborowa Vodka" product is now priced at "$120.00" and originally priced at "$140.00" + When I disable "Winter sale" catalog promotion + And I go to the price history of a variant with code "WYBOROWA_VODKA" + Then I should see 6 log entries in the catalog price history + And there should be a log entry on the 1st position with the "$140.00" selling price, "$140.00" original price and datetime of the price change + And there should be a log entry on the 2nd position with the "$14.00" selling price, "$140.00" original price and datetime of the price change + And there should be a log entry on the 3rd position with the "$120.00" selling price, "$140.00" original price and datetime of the price change + And there should be a log entry on the 4th position with the "$12.00" selling price, "$120.00" original price and datetime of the price change + And there should be a log entry on the 5th position with the "$100.00" selling price, "$120.00" original price and datetime of the price change + And there should be a log entry on the 6th position with the "$100.00" selling price, no original price and datetime of the price change diff --git a/features/product/viewing_product_in_admin_panel/accesing_product_edit_page_from_show_page.feature b/features/product/viewing_product_in_admin_panel/accesing_product_edit_page_from_show_page.feature index c3bd617541..16c52b3320 100644 --- a/features/product/viewing_product_in_admin_panel/accesing_product_edit_page_from_show_page.feature +++ b/features/product/viewing_product_in_admin_panel/accesing_product_edit_page_from_show_page.feature @@ -12,14 +12,14 @@ Feature: Accessing to product edit page from show page And I am logged in as an administrator And I am browsing products - @ui + @ui @no-api Scenario: Accessing to product edit page from product show page - When I access "Iron shield" product page + When I access the "Iron Shield" product And I go to edit page Then I should be on "Iron shield" product edit page - @ui + @ui @no-api Scenario: Accessing to variant edit page from product show page - When I access "Iron shield" product page + When I access the "Iron Shield" product And I go to edit page of "Iron shield - very big" variant Then I should be on "Iron shield - very big" variant edit page diff --git a/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature b/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature index 1462355658..50874a1713 100644 --- a/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature +++ b/features/product/viewing_product_in_admin_panel/checking_products_in_the_shop_while_viewing_them.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Checking products in the shop while viewing them In order to check a product in shop in all channels it is available in As an Administrator @@ -14,18 +14,18 @@ Feature: Checking products in the shop while viewing them @ui @no-api Scenario: Accessing product show page in shop from the product show page where product is available in more than one channel Given this product is available in the "Europe" channel - When I access "Bugatti" product page + When I access the "Bugatti" product And I show this product in the "Europe" channel Then I should see this product in the "Europe" channel in the shop @ui @no-api Scenario: Accessing product show page in shop from the product show page where product is available in one channel - When I access "Bugatti" product page + When I access the "Bugatti" product And I show this product in this channel Then I should see this product in the "Europe" channel in the shop @ui @no-api Scenario: Being unable to access product show page in shop when the product is disabled Given this product has been disabled - When I access "Bugatti" product page + When I access the "Bugatti" product Then I should not be able to show this product in shop diff --git a/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_for_a_simple_product.feature b/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_for_a_simple_product.feature index 607372ff25..af103dfa62 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_for_a_simple_product.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_for_a_simple_product.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Seeing applied catalog promotions details for a simple product In order to be aware of simple product price change reason As an Administrator @@ -6,15 +6,16 @@ Feature: Seeing applied catalog promotions details for a simple product Background: Given the store operates on a single channel in "United States" - And the store has a product "Ursus C-355" priced at "$1000.00" in "United States" channel + And the store has a product "Ursus C-355" priced at "$1,000.00" in "United States" channel And there is a catalog promotion with "company_bankruptcy_sale" code and "Company bankruptcy sale" name And the catalog promotion "Company bankruptcy sale" is available in "United States" And it applies on "Ursus C-355" product And it reduces price by "90%" And it is enabled And I am logged in as an administrator + And I am browsing products - @ui + @ui @no-api Scenario: Seeing applied catalog promotion details on a simple product When I access "Ursus C-355" product Then this product price should be decreased by catalog promotion "Company bankruptcy sale" in "United States" channel diff --git a/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_within_variant.feature b/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_within_variant.feature index 985d6b15a6..8994cca64e 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_within_variant.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_applied_catalog_promotions_details_within_variant.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Seeing applied catalog promotions details within variant In order to be aware of variant's price change reason As an Administrator @@ -20,11 +20,20 @@ Feature: Seeing applied catalog promotions details within variant And it reduces price by "37%" And it is enabled And I am logged in as an administrator + And I am browsing products - @ui + @ui @no-api Scenario: Seeing applied catalog promotion details within variant When I access "Wyborowa Vodka" product Then "Wyborowa Vodka Exquisite" variant price should be decreased by catalog promotion "Winter sale" in "United States" channel And "Wyborowa Vodka Lemon" variant price should not be decreased by catalog promotion "Winter sale" in "United States" channel And "Wyborowa Vodka Exquisite" variant price should not be decreased by catalog promotion "Christmas sale" in "United States" channel And "Wyborowa Vodka Lemon" variant price should be decreased by catalog promotion "Christmas sale" in "United States" channel + + @api @no-ui + Scenario: Seeing applied catalog promotion details within variant + When I view all variants of the product "Wyborowa Vodka" + Then "Wyborowa Vodka Exquisite" variant price should be decreased by catalog promotion "Winter sale" in "United States" channel + And "Wyborowa Vodka Lemon" variant price should not be decreased by catalog promotion "Winter sale" in "United States" channel + And "Wyborowa Vodka Exquisite" variant price should not be decreased by catalog promotion "Christmas sale" in "United States" channel + And "Wyborowa Vodka Lemon" variant price should be decreased by catalog promotion "Christmas sale" in "United States" channel diff --git a/features/product/viewing_product_in_admin_panel/viewing_details_of_product_with_variants.feature b/features/product/viewing_product_in_admin_panel/viewing_details_of_product_with_variants.feature index 00c2a6513a..e8844bf412 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_details_of_product_with_variants.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_details_of_product_with_variants.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Viewing details of a product with variants In order to view detailed product information As an Administrator @@ -17,55 +17,69 @@ Feature: Viewing details of a product with variants And I am logged in as an administrator And I am browsing products - @ui - Scenario: Viewing a configurable product show page - When I access "Iron Shield" product page + @ui @no-api + Scenario: Viewing a configurable product + When I access the "Iron Shield" product Then I should see product show page with variants And I should see product name "Iron Shield" - @ui - Scenario: Viewing taxonomy block + @ui @api + Scenario: Viewing taxonomies Given the store classifies its products as "Shield" and "Equipment" And the product "Iron Shield" has a main taxon "Equipment" And the product "Iron Shield" belongs to taxon "Shield" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see main taxon is "Equipment" - And I should see product taxon is "Shield" + And I should see product taxon "Shield" - @ui - Scenario: Viewing options block - When I access "Iron Shield" product page + @ui @api + Scenario: Viewing options + When I access the "Iron Shield" product Then I should see option "Shield size" - @ui - Scenario: Viewing variants block - When I access "Iron Shield" product page + @ui @api + Scenario: Viewing variants + When I access the "Iron Shield" product + Then I should see 2 variants + And I should see the "Iron Shield - very big" variant + And I should see the "Iron Shield - very small" variant + + @ui @no-api + Scenario: Viewing variants' details + When I access the "Iron Shield" product Then I should see 2 variants And I should see "Iron Shield - very big" variant with code "123456789-xl", priced "$25.00" and current stock 5 and in "United States" channel And I should see "Iron Shield - very small" variant with code "123456789-xs", priced "$15.00" and current stock 12 and in "United States" channel - @ui @javascript - Scenario: Viewing media block + @ui @javascript @api + Scenario: Viewing media Given the "Iron Shield" product has an image "mugs.jpg" with "main" type - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see an image related to this product - @ui - Scenario: Viewing "more details" block + @ui @api + Scenario: Viewing more details Given the product "Iron Shield" has the slug "iron-shield" And the description of product "Iron Shield" is "Shield created by dwarf" And the meta keywords of product "Iron Shield" is "shield" And the short description of product "Iron Shield" is "good shield" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product name is "Iron Shield" And I should see product slug is "iron-shield" And I should see product's description is "Shield created by dwarf" And I should see product's meta keywords is "shield" And I should see product's short description is "good shield" - @ui - Scenario: Viewing associations block + @ui @api + Scenario: Viewing association types Given the store has a "Glass shield" product And the product "Iron Shield" has an association "Similar" with product "Glass shield" - When I access "Iron Shield" product page + When I access the "Iron Shield" product + Then I should see product association type "Similar" + + @ui @no-api + Scenario: Viewing associations + Given the store has a "Glass shield" product + And the product "Iron Shield" has an association "Similar" with product "Glass shield" + When I access the "Iron Shield" product Then I should see product association "Similar" with "Glass shield" diff --git a/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature b/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature index 09b4b33c3a..71de4aff4a 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_details_of_simple_product.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Viewing details of a simple product In order to view detailed product information As an Administrator @@ -11,83 +11,83 @@ Feature: Viewing details of a simple product And I am logged in as an administrator And I am browsing products - @ui + @ui @no-api Scenario: Viewing a simple product show page - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product show page without variants And I should see product name "Iron Shield" And I should see product breadcrumb "Iron Shield" - @ui + @ui @no-api Scenario: Viewing pricing block Given the product "Iron Shield" has original price "$25.00" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see price "$20.00" for channel "United States" And I should see original price "$25.00" for channel "United States" - @ui + @ui @no-api Scenario: Viewing price block without channel enable Given this product is unavailable in "United States" channel - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product name "Iron Shield" And I should see the product in neither channel And I should not see price for channel "United States" - @ui + @ui @no-api Scenario: Viewing details block Given the store has a tax category "No tax" with a code "nt" And product's "Iron Shield" code is "123456789" And there are 4 units of product "Iron Shield" available in the inventory And the product "Iron Shield" belongs to "No tax" tax category - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product's code is "123456789" And I should see the product is enabled for channel "United States" And I should see 4 as a current stock of this product And I should see product's tax category is "No tax" - @ui + @ui @no-api Scenario: Viewing taxonomy block Given this product belongs to "Shield" And the product "Iron Shield" has a main taxon "Equipment" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see main taxon is "Equipment" - And I should see product taxon is "Shield" + And I should see product taxon "Shield" - @ui + @ui @no-api Scenario: Viewing shipping block Given the store has "Over sized" and "Standard" shipping category And the product "Iron Shield" has height "10.0", width "15.0", depth "20.0", weight "25.0" And this product belongs to "Over sized" shipping category - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product's shipping category is "Over sized" And I should see product's height is 10 And I should see product's width is 15 And I should see product's depth is 20 And I should see product's weight is 25 - @ui @javascript + @ui @javascript @api Scenario: Viewing media block Given the "Iron Shield" product has an image "mugs.jpg" with "main" type - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see an image related to this product - @ui + @ui @no-api Scenario: Viewing "more details" block Given the product "Iron Shield" has the slug "iron-shield" And the description of product "Iron Shield" is "Shield created by dwarf" And the meta keywords of product "Iron Shield" is "shield" And the short description of product "Iron Shield" is "good shield" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product name is "Iron Shield" And I should see product slug is "iron-shield" And I should see product's description is "Shield created by dwarf" And I should see product's meta keywords is "shield" And I should see product's short description is "good shield" - @ui + @ui @no-api Scenario: Viewing associations block Given the store has "Similar" and "Dwarf equipment" product association types And the store has a "Glass Shield" product And the product "Iron Shield" has an association "Similar" with product "Glass Shield" - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see product association "Similar" with "Glass Shield" diff --git a/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature new file mode 100644 index 0000000000..7cfb20af7c --- /dev/null +++ b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_for_simple_product.feature @@ -0,0 +1,22 @@ +@viewing_product_in_admin_panel +Feature: Seeing the lowest price before the discount for a simple product + In order to be aware of simple product prices + As an Administrator + I want to see details of the lowest price before the discount nearby product's price + + Background: + Given the store operates on a single channel in "United States" + And the store has a product "Bizon Z056" priced at "$42.00" in "United States" channel + And I am logged in as an administrator + And I am browsing products + + @ui @no-api + Scenario: Seeing price block with lowest price before the discount + Given this product's price changed to "$21.00" and original price changed to "$37.00" + When I access the "Bizon Z056" product + Then I should see "$42.00" as its lowest price before the discount in "United States" channel + + @ui @no-api + Scenario: Seeing price block without lowest price before the discount + When I access the "Bizon Z056" product + Then I should not see the lowest price before the discount in "United States" channel diff --git a/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature new file mode 100644 index 0000000000..b1e723f55d --- /dev/null +++ b/features/product/viewing_product_in_admin_panel/viewing_lowest_price_before_discount_within_variant.feature @@ -0,0 +1,20 @@ +@viewing_product_in_admin_panel +Feature: Seeing the lowest price before the discount within variant + In order to be aware of variant's prices + As an Administrator + I want to see details of the lowest price before the discount nearby variant's price + + Background: + Given the store operates on a single channel in "United States" + And the store has a "Wyborowa Vodka" configurable product + And the product "Wyborowa Vodka" has a "Wyborowa Vodka Rye" variant priced at "$9.00" + And the product "Wyborowa Vodka" has a "Wyborowa Vodka Potato" variant priced at "$11.00" + And this variant's price changed to "$21.00" and original price changed to "$37.00" + And I am logged in as an administrator + And I am browsing products + + @ui @no-api + Scenario: Seeing price block with lowest price before the discount within variant + When I access the "Wyborowa Vodka" product + Then I should not see the lowest price before the discount for "Wyborowa Vodka Rye" variant in "United States" channel + And I should see the lowest price before the discount of "$11.00" for "Wyborowa Vodka Potato" variant in "United States" channel diff --git a/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature b/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature index c9324be56f..ebff161863 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_non_translatable_attributes_admin.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Viewing product's non translatable attributes In order to see product's non translatable attribute As an Administrator @@ -14,9 +14,9 @@ Feature: Viewing product's non translatable attributes And I am logged in as an administrator And I am browsing products - @ui + @ui @api Scenario: Viewing product's non translatable attributes along with default ones - When I access "Iron Pickaxe" product page - Then I should see non-translatable attribute "crit chance" with value "10 %" + When I access the "Iron Pickaxe" product + Then I should see non-translatable attribute "crit chance" with value 10% And I should see attribute "Material" with value "Iron" in "English (United States)" locale And I should see attribute "Material" with value "Żelazo" in "Polish (Poland)" locale diff --git a/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature b/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature index 26410e8f8c..7d00084b0a 100644 --- a/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature +++ b/features/product/viewing_product_in_admin_panel/viewing_product_attributes_in_different_locales.feature @@ -1,4 +1,4 @@ -@viewing_products +@viewing_product_in_admin_panel Feature: Viewing product's attributes in different locales In order to see product's specification in all locales As a Administrator @@ -15,9 +15,9 @@ Feature: Viewing product's attributes in different locales And I am logged in as an administrator And I am browsing products - @ui + @ui @api Scenario: Viewing product's attributes defined in different locales - When I access "Iron Shield" product page + When I access the "Iron Shield" product Then I should see attribute "material" with value "oak wood" in "English (United States)" locale And I should see attribute "shield details" with value "oak wood is a very good material." in "English (United States)" locale And I should see attribute "material" with value "drewno dębowe" in "Polish (Poland)" locale diff --git a/features/product/viewing_product_variants/viewing_list_of_product_variants_with_corresponding_prices_and_options.feature b/features/product/viewing_product_variants/viewing_list_of_product_variants_with_corresponding_prices_and_options.feature new file mode 100644 index 0000000000..6cccf3616c --- /dev/null +++ b/features/product/viewing_product_variants/viewing_list_of_product_variants_with_corresponding_prices_and_options.feature @@ -0,0 +1,28 @@ +@viewing_product_variants +Feature: Viewing corresponding prices and options of product variants + In order to distinguish between product variants + As a Visitor + I want to be able to view corresponding prices and options of product variants + + Background: + Given the store operates on a channel named "Web-US" in "USD" currency + And the store has a "Raspberry Pi" configurable product + And this product has option "Memory" with values "1GB", "2GB" and "4GB" + And this product with "memory" option "1GB" is priced at "$21.00" + And this product with "memory" option "2GB" is priced at "$42.00" + And this product with "memory" option "4GB" is priced at "$84.00" + + @api @no-ui + Scenario: Viewing product variants with corresponding prices and options + When I view variants of the "Raspberry Pi" product + Then I should see variant with "Memory" option and "1GB" option value priced at "$21.00" at 1st position + And I should see variant with "Memory" option and "2GB" option value priced at "$42.00" at 2nd position + And I should see variant with "Memory" option and "4GB" option value priced at "$84.00" at 3rd position + + @api @no-ui + Scenario: Filtering product variants by option + When I view variants of the "Raspberry Pi" product + And I filter variants by "2GB" option value + Then I should see variant with "Memory" option and "2GB" option value priced at "$42.00" at 1st position + And I should not see variant with "Memory" option "1GB" + And I should not see variant with "Memory" option "4GB" diff --git a/features/product/viewing_products/accessing_disabled_taxon.feature b/features/product/viewing_products/accessing_disabled_taxon.feature index 8ada7ac8f5..497c85942e 100644 --- a/features/product/viewing_products/accessing_disabled_taxon.feature +++ b/features/product/viewing_products/accessing_disabled_taxon.feature @@ -7,9 +7,17 @@ Feature: Accessing a disabled taxon Background: Given the store operates on a single channel in "United States" - @ui + @ui @no-api Scenario: Accessing a disabled taxon Given the store has "T-Shirts" taxonomy And the "T-Shirts" taxon is disabled When I try to browse products from taxon "T-Shirts" Then I should be informed that the taxon does not exist + + @api @no-ui + Scenario: Filtering products by taxon available only for enabled taxon + Given the store has "Food" taxonomy + Given the store has a product "Baguette" priced at "$2.00" belonging to the "Food" taxon + And the "Food" taxon is disabled + When I browse products from product taxon code "Food" + Then I should see empty list of products diff --git a/features/product/viewing_products/accessing_product_wich_does_not_exist.feature b/features/product/viewing_products/accessing_product_wich_does_not_exist.feature index 367811011b..a216707160 100644 --- a/features/product/viewing_products/accessing_product_wich_does_not_exist.feature +++ b/features/product/viewing_products/accessing_product_wich_does_not_exist.feature @@ -7,7 +7,7 @@ Feature: Accessing a product which does not exist Background: Given the store operates on a single channel in "United States" - @ui + @ui @api Scenario: Accessing a product which does not exist When I try to reach unexistent product Then I should be informed that the product does not exist diff --git a/features/product/viewing_products/not_seeing_lowest_price_for_product_that_has_taxon_excluded_on_channel.feature b/features/product/viewing_products/not_seeing_lowest_price_for_product_that_has_taxon_excluded_on_channel.feature new file mode 100644 index 0000000000..bb90bfff11 --- /dev/null +++ b/features/product/viewing_products/not_seeing_lowest_price_for_product_that_has_taxon_excluded_on_channel.feature @@ -0,0 +1,40 @@ +@viewing_products +Feature: Not seeing the lowest price for a product that has a taxon excluded on the channel + In order to show the product's lowest price only in applicable taxons + As a Guest + I don't want to see the product's lowest price in taxons that have been excluded on the channel + + Background: + Given the store operates on a single channel in "United States" + And the store classifies its products as "Category" + And the "Category" taxon has children taxons "Groceries" and "Special offers" + And the "Groceries" taxon has child taxon "Vegetables" + And the store has a product "Broccoli" priced at "$20.00" belonging to the "Vegetables" taxon + And this product's price changed to "$10.00" and original price changed to "$20.00" + And the store also has a product "Cauliflower" priced at "$25.00" + And it belongs to "Vegetables" and "Special offers" + And this product's price changed to "$15.00" and original price changed to "$25.00" + + @api @ui + Scenario: Not seeing the lowest price for a product that has a taxon excluded on the channel + Given the "Vegetables" taxon is excluded from showing the lowest price of discounted products in the "United States" channel + When I view product "Broccoli" + Then I should not see information about its lowest price + + @api @ui + Scenario: Not seeing the lowest price for a product that has parent taxon excluded on the channel + Given the "Groceries" taxon is excluded from showing the lowest price of discounted products in the "United States" channel + When I view product "Broccoli" + Then I should not see information about its lowest price + + @api @ui + Scenario: Not seeing the lowest price for a product that has root taxon excluded on the channel + Given the "Category" taxon is excluded from showing the lowest price of discounted products in the "United States" channel + When I view product "Broccoli" + Then I should not see information about its lowest price + + @api @ui + Scenario: Not seeing the lowest price for a product that has only one taxon excluded on the channel + Given the "Vegetables" taxon is excluded from showing the lowest price of discounted products in the "United States" channel + When I view product "Cauliflower" + Then I should not see information about its lowest price diff --git a/features/product/viewing_products/redirecting_to_products_when_there_is_a_trailing_slash_in_path.feature b/features/product/viewing_products/redirecting_to_products_when_there_is_a_trailing_slash_in_path.feature index 39a3078ffa..bc80f27203 100644 --- a/features/product/viewing_products/redirecting_to_products_when_there_is_a_trailing_slash_in_path.feature +++ b/features/product/viewing_products/redirecting_to_products_when_there_is_a_trailing_slash_in_path.feature @@ -13,7 +13,7 @@ Feature: Redirecting to products when there is a trailing slash in path And the store has a product "Plastic Tomato" available in "Poland" channel And this product belongs to "Funny" - @ui + @ui @no-api Scenario: Redirecting to products when there is a trailing slash in path When I try to browse products from taxon "T-Shirts" with a trailing slash in the path Then I should be redirected on the product list from taxon "T-Shirts" diff --git a/features/product/viewing_products/seeing_correct_lowest_prices_when_period_changes_on_channel.feature b/features/product/viewing_products/seeing_correct_lowest_prices_when_period_changes_on_channel.feature new file mode 100644 index 0000000000..e61ec005e1 --- /dev/null +++ b/features/product/viewing_products/seeing_correct_lowest_prices_when_period_changes_on_channel.feature @@ -0,0 +1,51 @@ +@viewing_products +Feature: Seeing the correct product's lowest price according to the period set on the channel + In order to know whether the current discount is attractive + As a Guest + I want to see a product's lowest price within the channel's set period + + Background: + Given the store operates on a single channel in "United States" + And it is "2023-01-01" now + And the store has a product "Wyborowa Vodka" priced at "$10.00" + And on "2023-01-05" its price changed to "$9.00" + And on "2023-02-05" its price changed to "$11.00" + And on "2023-02-15" its price changed to "$13.00" and original price to "$10.00" + And on "2023-02-25" its price changed to "$15.00" and original price to "$20.00" + And it is "2023-03-01" now + + @api @ui + Scenario: Seeing the correct lowest price from 60 days before the discount + Given this channel has 60 days set as the lowest price for discounted products checking period + When I check this product's details + Then I should see "$9.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the correct lowest price from 30 days before the discount + Given this channel has 30 days set as the lowest price for discounted products checking period + When I check this product's details + Then I should see "$9.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the correct lowest price from 20 days before the discount + Given this channel has 20 days set as the lowest price for discounted products checking period + When I check this product's details + Then I should see "$9.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the correct lowest price from 10 days before the discount + Given this channel has 10 days set as the lowest price for discounted products checking period + When I check this product's details + Then I should see "$11.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the correct lowest price from 5 days before the discount + Given this channel has 5 days set as the lowest price for discounted products checking period + When I check this product's details + Then I should see "$13.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the correct lowest price from 1 day before the discount + Given this channel has 1 day set as the lowest price for discounted products checking period + When I check this product's details + Then I should see "$13.00" as its lowest price before the discount diff --git a/features/product/viewing_products/seeing_corresponding_lowest_price_before_discount_when_selecting_different_product_option.feature b/features/product/viewing_products/seeing_corresponding_lowest_price_before_discount_when_selecting_different_product_option.feature new file mode 100644 index 0000000000..6773ad2226 --- /dev/null +++ b/features/product/viewing_products/seeing_corresponding_lowest_price_before_discount_when_selecting_different_product_option.feature @@ -0,0 +1,56 @@ +@viewing_products +Feature: Seeing the corresponding lowest price before the discount when selecting different product's options + In order to be aware of the lowest price before the discount for the chosen product option + As a Customer + I want to see the corresponding lowest price before the discount for the chosen product option + + Background: + Given the store operates on a single channel in "United States" + And the store has a "Wyborowa Vodka" configurable product + And this product has option "Taste" with values "Exquisite", "Lemon" and "Bitter" + And this product has "Wyborowa-Vodka-1" variant priced at "$20.00" configured with "EXQUISITE" option value + And this product has "Wyborowa-Vodka-2" variant priced at "$25.00" configured with "LEMON" option value + And this product has "Wyborowa-Vodka-3" variant priced at "$35.00" configured with "BITTER" option value + And this product is configured with the option matching selection method + And the store has a "Bocian Vodka" configurable product + And this product has option "Size" with values "Small" and "Medium" + And this product has option "Color" with values "Blue" and "Green" + And this product has all possible variants priced at "$10.00" with indexed names + And this product is configured with the option matching selection method + And there is a catalog promotion "Winter sale" with priority 1 that reduces price by "50%" and applies on "Wyborowa Vodka" product + And there is a catalog promotion "Summer sale" with priority 1 that reduces price by "50%" and applies on "Bocian Vodka variant 0" variant + + + @no-api @ui @javascript + Scenario: Seeing correct lowest price when selecting first option value from the list + When I view product "Wyborowa Vodka" + And I select its "Taste" as "Exquisite" + Then I should see "$20.00" as its lowest price before the discount + + @no-api @ui @javascript + Scenario: Seeing correct lowest price when selecting another option value from the list + When I view product "Wyborowa Vodka" + And I select its "Taste" as "Lemon" + Then I should see "$25.00" as its lowest price before the discount + + @no-api @ui @javascript + Scenario: Seeing correct lowest price when selecting last option value from the list after selecting another option value from the list + When I view product "Wyborowa Vodka" + And I select its "Taste" as "Lemon" + And I select its "Taste" as "Exquisite" + And I select its "Taste" as "Bitter" + Then I should see "$35.00" as its lowest price before the discount + + @no-api @ui @javascript + Scenario: Seeing correct lowest price when having discounted variant with more than one option value + When I view product "Bocian Vodka" + And I select its "Color" as "Blue" + And I select its "Size" as "Small" + Then I should see "$10.00" as its lowest price before the discount + + @no-api @ui @javascript + Scenario: Not seeing the lowest price when having variant with more than one option value and without discount + When I view product "Bocian Vodka" + And I select its "Color" as "Blue" + And I select its "Size" as "Medium" + Then I should not see information about its lowest price diff --git a/features/product/viewing_products/seeing_corresponding_lowest_price_before_discount_when_selecting_different_product_variant.feature b/features/product/viewing_products/seeing_corresponding_lowest_price_before_discount_when_selecting_different_product_variant.feature new file mode 100644 index 0000000000..9357a90e79 --- /dev/null +++ b/features/product/viewing_products/seeing_corresponding_lowest_price_before_discount_when_selecting_different_product_variant.feature @@ -0,0 +1,33 @@ +@viewing_products +Feature: Seeing the corresponding lowest price before the discount when selecting different product variants + In order to be aware of the lowest price before the discount for the chosen product variant + As a Customer + I want to see the corresponding lowest price before the discount for the chosen product variant + + Background: + Given the store operates on a single channel in "United States" + And the store has a "Wyborowa Vodka" configurable product + And the product "Wyborowa Vodka" has "Wyborowa Vodka 40%" variant priced at "$40.00" + And the product "Wyborowa Vodka" has "Wyborowa Vodka 50%" variant priced at "$50.00" + And the product "Wyborowa Vodka" has "Wyborowa Vodka 30%" variant priced at "$60.00" + And there is a catalog promotion "Winter sale" with priority 1 that reduces price by "50%" and applies on "Wyborowa Vodka" product + + @no-api @ui @javascript + Scenario: Seeing correct lowest price when selecting first variant from the list + When I view product "Wyborowa Vodka" + And I select "Wyborowa Vodka 40%" variant + Then I should see "$40.00" as its lowest price before the discount + + @no-api @ui @javascript + Scenario: Seeing correct lowest price when selecting another variant from the list + When I view product "Wyborowa Vodka" + And I select "Wyborowa Vodka 50%" variant + Then I should see "$50.00" as its lowest price before the discount + + @no-api @ui @javascript + Scenario: Seeing correct lowest price when selecting first variant from the list after selecting another variant + When I view product "Wyborowa Vodka" + And I select "Wyborowa Vodka 50%" variant + And I select "Wyborowa Vodka 30%" variant + And I select "Wyborowa Vodka 40%" variant + Then I should see "$40.00" as its lowest price before the discount diff --git a/features/product/viewing_products/seeing_products_lowest_price_before_discount.feature b/features/product/viewing_products/seeing_products_lowest_price_before_discount.feature new file mode 100644 index 0000000000..63fb7e497c --- /dev/null +++ b/features/product/viewing_products/seeing_products_lowest_price_before_discount.feature @@ -0,0 +1,136 @@ +@viewing_products +Feature: Seeing the product's lowest price before the discount + In order to know whether the current discount is attractive + As a Guest + I want to see a product's lowest price before the discount + + Background: + Given the store operates on a single channel in "United States" + + @api @ui + Scenario: Not seeing the lowest price information on a product with no discount + Given the store has a product "Wyborowa Vodka" priced at "$21.00" + When I check this product's details + Then I should not see information about its lowest price + + @api @ui + Scenario: Seeing the lowest price information on a product that has a single discount + Given the store has a product "Wyborowa Vodka" priced at "$37.00" + And this product's price changed to "$21.00" and original price changed to "$37.00" + When I check this product's details + Then I should see "$37.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the lowest price information on a product that has a discount, and then seeing the discount change + Given the store has a product "Wyborowa Vodka" priced at "$37.00" + And this product's price changed to "$21.00" and original price changed to "$37.00" + And this product's price changed to "$30.00" + When I check this product's details + Then I should see "$21.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the lowest price information on a product that has a discount, over a month passes, and then seeing the discount change + Given it is "2023-03-14" now + And the store has a product "Wyborowa Vodka" priced at "$42.00" + And this product's price changed to "$21.00" and original price changed to "$42.00" + And it is "2023-04-15" now + And this product's price changed to "$33.00" + When I check this product's details + Then I should see "$21.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the lowest price information on a product that has a discount and the one of the previous promotions ended over 30 days ago + Given it is "2022-12-01" now + And the store has a product "Wyborowa Vodka" priced at "$42.00" + And this product's price changed to "$21.00" and original price changed to "$42.00" + And it is "2023-01-01" now + And this product's price changed to "$39.00" + And it is "2023-02-15" now + And this product's price changed to "$33.00" + When I check this product's details + Then I should see "$39.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the lowest price information on a product that has a discount and the previous promotion ended over 30 days ago + Given it is "2022-12-01" now + And the store has a product "Wyborowa Vodka" priced at "$42.00" + And this product's price changed to "$21.00" and original price changed to "$42.00" + And it is "2023-01-01" now + And this product's price changed to "$42.00" and original price was removed + And it is "2023-02-15" now + And this product's price changed to "$33.00" and original price changed to "$42.00" + When I check this product's details + Then I should see "$42.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the lowest price information on a product that has the lowest price set beyond the period but it is still the lowest price in the period + Given it is "2023-01-01" now + And the store has a product "Wyborowa Vodka" priced at "$10.00" + And on "2023-01-05" its price changed to "$9.00" + And on "2023-02-05" its price changed to "$11.00" + And on "2023-02-15" its price changed to "$13.00" and original price to "$10.00" + And on "2023-02-25" its price changed to "$15.00" and original price to "$20.00" + And it is "2023-03-01" now + When I check this product's details + Then I should see "$9.00" as its lowest price before the discount + + @api @ui + Scenario: Not seeing the lowest price information on a product that had a discount and the discount was removed + Given the store has a product "Wyborowa Vodka" priced at "$37.00" + And this product's price changed to "$21.00" and original price changed to "$37.00" + And this product's price changed to "$37.00" and original price was removed + When I check this product's details + Then I should not see information about its lowest price + + @api @ui + Scenario: Seeing the lowest price information on a product that had a discount, the discount was removed, the price changed below the discount price, the price changed back to the start value and then the less attractive discount was added + Given the store has a product "Wyborowa Vodka" priced at "$37.00" + And this product's price changed to "$21.00" and original price changed to "$37.00" + And this product's price changed to "$37.00" and original price was removed + And this product's price changed to "$10.00" + And this product's price changed to "$21.00" + And this product's price changed to "$20.00" and original price changed to "$21.00" + When I check this product's details + Then I should see "$10.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the lowest price information on a product that had a discount, the price was changed below previous discount while a less attractive discount was added and a less attractive discount was added + Given the store has a product "Wyborowa Vodka" priced at "$37.00" + And this product's price changed to "$20.00" and original price changed to "$37.00" + And this product's price changed to "$21.00" and original price changed to "$19.00" + And this product's price changed to "$33.00" and original price changed to "$37.00" + When I check this product's details + Then I should see "$20.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the lowest price information on a product with same discount repeated + Given the store has a product "Wyborowa Vodka" priced at "$37.00" + And this product's price changed to "$20.00" and original price changed to "$37.00" + And this product's price changed to "$37.00" and original price was removed + And this product's price changed to "$20.00" and original price changed to "$37.00" + When I check this product's details + Then I should see "$20.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the lowest price information on a product with catalog promotion + Given the store has a product "Wyborowa Vodka" priced at "$37.00" + And there is a catalog promotion "Winter Sale" with priority 100 that reduces price by "$10.00" and applies on "Wyborowa Vodka" product + When I check this product's details + Then I should see "$37.00" as its lowest price before the discount + + @api @ui + Scenario: Seeing the lowest price information on a product with catalog promotion and reduced price + Given the store has a product "Wyborowa Vodka" priced at "$37.00" + And this product's price changed to "$20.00" + And there is a catalog promotion "Winter Sale" with priority 100 that reduces price by "$10.00" and applies on "Wyborowa Vodka" product + When I check this product's details + Then I should see "$20.00" as its lowest price before the discount + + @api @ui + Scenario: Not seeing the lowest price information on a product that has a discount but showing it is disabled on the channel + Given the lowest price of discounted products prior to the current discount is disabled on this channel + And the store has a product "Wyborowa Vodka" priced at "$37.00" + And this product's price changed to "$21.00" and original price changed to "$37.00" + And this product's price changed to "$30.00" + When I check this product's details + Then I should not see information about its lowest price diff --git a/features/product/viewing_products/viewing_children_taxons_from_specific_taxon.feature b/features/product/viewing_products/viewing_children_taxons_from_specific_taxon.feature index 710ea47e3c..a83f81fbe3 100644 --- a/features/product/viewing_products/viewing_children_taxons_from_specific_taxon.feature +++ b/features/product/viewing_products/viewing_children_taxons_from_specific_taxon.feature @@ -11,14 +11,14 @@ Feature: Viewing children taxons of current taxon And the "Clothes" taxon has children taxons "T-Shirts", "Coats" and "Trousers" And channel "United States" has menu taxon "Category" - @ui + @ui @api Scenario: Viewing only enabled taxons in the vertical menu Given the "Coats" taxon is disabled When I try to browse products from taxon "Clothes" Then I should not see "Coats" in the vertical menu And I should see "T-Shirts" and "Trousers" in the vertical menu - @ui + @ui @no-api Scenario: Cannot navigate to disabled parent taxon Given the "Clothes" taxon is disabled When I try to browse products from taxon "T-Shirts" diff --git a/features/product/viewing_products/viewing_diagonal_variant_options.feature b/features/product/viewing_products/viewing_diagonal_variant_options.feature index 34798306b7..0788dcfd4f 100644 --- a/features/product/viewing_products/viewing_diagonal_variant_options.feature +++ b/features/product/viewing_products/viewing_diagonal_variant_options.feature @@ -17,16 +17,23 @@ Feature: Viewing diagonal variants options But the "Small" size / "Blue" color variant of product "Extra Cool T-Shirt" is disabled And the "Large" size / "Yellow" color variant of product "Extra Cool T-Shirt" is disabled - @ui + @api @ui Scenario: Viewing both values for both options when diagonal variants are available When I view product "Extra Cool T-Shirt" - Then I should be able to select the "Yellow" and "Blue" color option values - And I should be able to select the "Small" and "Large" size option values + Then I should be able to select the "Yellow" and "Blue" Color option values + And I should be able to select the "Small" and "Large" Size option values - @ui @javascript + @ui @javascript @no-api Scenario: Viewing an "Unavailable" message when selecting an unavailable combination When I view product "Extra Cool T-Shirt" And I select its color as "Blue" And I select its size as "Small" Then I should see that the combination is "Unavailable" And I should be unable to add it to the cart + + @api @no-ui + Scenario: Not seeing unavailable variants + When I view variants of the "Extra Cool T-Shirt" product + And I filter them by "Blue" option value + And I filter them by "Small" option value + Then I should not see any variants diff --git a/features/product/viewing_products/viewing_different_price_for_different_product_variants.feature b/features/product/viewing_products/viewing_different_price_for_different_product_variants.feature index eedc1ff1cb..c85f1877cd 100644 --- a/features/product/viewing_products/viewing_different_price_for_different_product_variants.feature +++ b/features/product/viewing_products/viewing_different_price_for_different_product_variants.feature @@ -17,8 +17,8 @@ Feature: Viewing different price for different product variants When I view product "Wyborowa Vodka" Then the product price should be "$40.00" - @ui @javascript + @ui @api @javascript Scenario: Viewing a detailed page with product's price for different variant When I view product "Wyborowa Vodka" And I select "Wyborowa Apple" variant - Then the product price should be "$12.55" + Then this product variant price should be "$12.55" diff --git a/features/product/viewing_products/viewing_different_price_for_different_product_variants_selected_with_options.feature b/features/product/viewing_products/viewing_different_price_for_different_product_variants_selected_with_options.feature index a5ae2f31fd..fee81ab07f 100644 --- a/features/product/viewing_products/viewing_different_price_for_different_product_variants_selected_with_options.feature +++ b/features/product/viewing_products/viewing_different_price_for_different_product_variants_selected_with_options.feature @@ -11,12 +11,12 @@ Feature: Viewing different price for different product variants selected with op And this product is available in "0,5L" volume priced at "$20.00" And this product is available in "0,7L" volume priced at "$25.00" - @ui + @ui @no-api Scenario: Viewing a detailed page with product's price When I view product "Wyborowa Vodka" Then I should see the product price "$20.00" - @ui @javascript + @ui @javascript @no-api Scenario: Viewing a detailed page with product's price for different option When I view product "Wyborowa Vodka" And I select its volume as "0,7L" diff --git a/features/product/viewing_products/viewing_product_associations.feature b/features/product/viewing_products/viewing_product_associations.feature index 52c1193cd8..89e1699b1a 100644 --- a/features/product/viewing_products/viewing_product_associations.feature +++ b/features/product/viewing_products/viewing_product_associations.feature @@ -24,21 +24,21 @@ Feature: Viewing product's associations Then I should see the product association "Accessories" with products "LG headphones" and "LG earphones" And I should also see the product association "Alternatives" with products "LG G4" and "LG G5" - @ui + @ui @api Scenario: Viewing a detailed page with product's associations after locale change Given I am browsing channel "Smartphone Store" When I view product "LG G3" in the "Polish (Poland)" locale Then I should see the product association "Akcesoria" with products "LG headphones" and "LG earphones" And I should also see the product association "Alternatywy" with products "LG G4" and "LG G5" - @ui + @ui @api Scenario: Viewing a detailed page with product's associations within current channel Given I am browsing channel "Notebook Store" When I view product "LG Gram" Then I should see the product association "Alternatives" with product "LG AC Adapter" And I should not see the product association "Alternatives" with product "LG headphones" - @ui + @ui @api Scenario: Viewing a detailed page with enabled associated products only Given the "LG G4" product is disabled And I am browsing channel "Smartphone Store" @@ -47,7 +47,7 @@ Feature: Viewing product's associations And I should also see the product association "Alternatives" with product "LG G5" And I should not see the product association "Alternatives" with product "LG G4" - @ui + @ui @api Scenario: Viewing a detailed page while an empty association exists Given products "LG G4" and "LG G5" are disabled And I am browsing channel "Smartphone Store" diff --git a/features/product/viewing_products/viewing_product_enabled_variants_options_only.feature b/features/product/viewing_products/viewing_product_enabled_variants_options_only.feature index 62f529ca92..63afe17d4d 100644 --- a/features/product/viewing_products/viewing_product_enabled_variants_options_only.feature +++ b/features/product/viewing_products/viewing_product_enabled_variants_options_only.feature @@ -10,9 +10,9 @@ Feature: Viewing product's enabled variants only And this product has option "Size" with values "Small", "Medium" and "Large" And this product has option "Color" with values "Blue", "Green" and "Yellow" And this product has all possible variants - But all the product variants with the "Yellow" color are disabled + But all the product variants with the "Yellow" Color are disabled @ui @api Scenario: Seeing only enabled variants options When I view product "Super Cool T-Shirt" - Then I should not be able to select the "Yellow" color option value + Then I should not be able to select the "Yellow" Color option value diff --git a/features/product/viewing_products/viewing_product_with_all_variants_disabled.feature b/features/product/viewing_products/viewing_product_with_all_variants_disabled.feature index 574bc93cb5..44c52ccb23 100644 --- a/features/product/viewing_products/viewing_product_with_all_variants_disabled.feature +++ b/features/product/viewing_products/viewing_product_with_all_variants_disabled.feature @@ -12,12 +12,12 @@ Feature: Viewing product with all variants disabled And all variants of this product are disabled And this product belongs to "T-Shirts" - @ui + @ui @api Scenario: Viewing product with all variants disabled When I check this product's details Then I should see the product name "Super Cool T-Shirt" - @ui + @ui @api Scenario: Viewing product with all variants disabled from taxon page When I browse products from taxon "T-Shirts" Then I should see the product "Super Cool T-Shirt" diff --git a/features/product/viewing_products/viewing_select_product_attributes.feature b/features/product/viewing_products/viewing_select_product_attributes.feature index a3d30d2f27..6e5e6f013c 100644 --- a/features/product/viewing_products/viewing_select_product_attributes.feature +++ b/features/product/viewing_products/viewing_select_product_attributes.feature @@ -17,24 +17,24 @@ Feature: Viewing product's select attributes Then I should see the product attribute "T-Shirt material" with value "Banana skin" on the list And I should also see the product attribute "T-Shirt material" with value "Cotton" on the list - @ui + @ui @api Scenario: Viewing a detailed page with product's select attribute after changing a value Given this product has select attribute "T-Shirt material" with values "Banana skin" and "Cotton" - When the administrator changes this product attribute's value "Cotton" to "Orange skin" + When this product attribute's value changed from "Cotton" to "Orange skin" And I check this product's details Then I should see the product attribute "T-Shirt material" with value "Banana skin" on the list And I should also see the product attribute "T-Shirt material" with value "Orange skin" on the list - @ui @javascript + @ui @javascript @api @no-postgres Scenario: Viewing a detailed page with product's select attribute after removing an only value Given this product has select attribute "T-Shirt material" with value "Cotton" - When the administrator deletes the value "Cotton" from this product attribute + When this product attribute's value "Cotton" has been removed And I check this product's details Then I should not see the product attribute "T-Shirt material" - @ui @javascript + @ui @javascript @api @no-postgres Scenario: Viewing a detailed page with product's select attribute after removing one of the value Given this product has select attribute "T-Shirt material" with values "Banana skin" and "Cotton" - When the administrator deletes the value "Cotton" from this product attribute + When this product attribute's value "Cotton" has been removed And I check this product's details Then I should see the product attribute "T-Shirt material" with value "Banana skin" diff --git a/features/promotion/applying_catalog_promotions/applying_catalog_promotions_price_calculation.feature b/features/promotion/applying_catalog_promotions/applying_catalog_promotions_price_calculation.feature index cd7b8a721c..9d5d2c9a96 100644 --- a/features/promotion/applying_catalog_promotions/applying_catalog_promotions_price_calculation.feature +++ b/features/promotion/applying_catalog_promotions/applying_catalog_promotions_price_calculation.feature @@ -7,7 +7,7 @@ Feature: Applying percentage catalog promotions Background: Given the store operates on a single channel in "United States" And the store classifies its products as "Soft Drinks" - And the store has a product "Orange Juice" priced at "$20" + And the store has a product "Orange Juice" priced at "$20.00" And this product belongs to "Soft Drinks" And the store has a product "Apple Juice" priced at "$20.25" And this product belongs to "Soft Drinks" diff --git a/features/promotion/applying_catalog_promotions/not_reapplying_catalog_promotions_on_variants_once_products_taxon_changes.feature b/features/promotion/applying_catalog_promotions/not_reapplying_catalog_promotions_on_variants_once_products_taxon_changes.feature index 5088aad71d..c9cd8bf966 100644 --- a/features/promotion/applying_catalog_promotions/not_reapplying_catalog_promotions_on_variants_once_products_taxon_changes.feature +++ b/features/promotion/applying_catalog_promotions/not_reapplying_catalog_promotions_on_variants_once_products_taxon_changes.feature @@ -16,16 +16,16 @@ Feature: Not reapplying catalog promotions on variants once the product’s taxo And there is disabled catalog promotion "Surprise sale" between "2021-07-01" and "2022-05-04" available in "Web-US" channel that reduces price by "90%" and applies on "Dishes" taxon And I am logged in as an administrator - @ui + @api @ui Scenario: Changing products taxon to taxon with scheduled catalog promotion When I change that the "T-Shirt" product does not belong to the "Clothes" taxon - And I change that the "T-Shirt" product belongs to the "Shirts" taxon + And I add "Shirts" taxon to the "T-Shirt" product Then the visitor should see that the "PHP T-Shirt" variant is not discounted And the visitor should still see "$100.00" as the price of the "T-Shirt" product in the "Web-US" channel - @ui + @api @ui Scenario: Changing products taxon to taxon with disabled catalog promotion When I change that the "T-Shirt" product does not belong to the "Clothes" taxon - And I change that the "T-Shirt" product belongs to the "Dishes" taxon + And I add "Dishes" taxon to the "T-Shirt" product Then the visitor should see that the "PHP T-Shirt" variant is not discounted And the visitor should still see "$100.00" as the price of the "T-Shirt" product in the "Web-US" channel diff --git a/features/promotion/applying_catalog_promotions/reapplying_catalog_promotions_on_variants_once_products_taxon_changes.feature b/features/promotion/applying_catalog_promotions/reapplying_catalog_promotions_on_variants_once_products_taxon_changes.feature index 7e057604c2..b9b3f3f1e3 100644 --- a/features/promotion/applying_catalog_promotions/reapplying_catalog_promotions_on_variants_once_products_taxon_changes.feature +++ b/features/promotion/applying_catalog_promotions/reapplying_catalog_promotions_on_variants_once_products_taxon_changes.feature @@ -16,12 +16,12 @@ Feature: Reapplying catalog promotions on variants once the product’s taxon ch And there is another catalog promotion "Summer sale" that reduces price by "50%" and applies on "Dishes" taxon And I am logged in as an administrator - @ui + @api @ui Scenario: Removing a taxon from a product When I change that the "T-Shirt" product does not belong to the "Clothes" taxon Then the visitor should see that the "PHP T-Shirt" variant is not discounted - @ui + @api @ui Scenario: Adding a taxon to a product - When I change that the "Mug" product belongs to the "Dishes" taxon + When I assign the "Dishes" taxon to the "Mug" product Then the visitor should see that the "PHP Mug" variant is discounted from "$10.00" to "$5.00" with "Summer sale" promotion diff --git a/features/promotion/applying_promotion_rules/reapplying_promotion_on_cart_change.feature b/features/promotion/applying_promotion_rules/reapplying_promotion_on_cart_change.feature index fed0da67a3..8424ba9d77 100644 --- a/features/promotion/applying_promotion_rules/reapplying_promotion_on_cart_change.feature +++ b/features/promotion/applying_promotion_rules/reapplying_promotion_on_cart_change.feature @@ -10,7 +10,7 @@ Feature: Reapplying promotion on cart change And there is a promotion "Holiday promotion" And I am a logged in customer - @ui + @ui @api Scenario: Not receiving discount on shipping after removing last item from cart Given the store has "DHL" shipping method with "$10.00" fee And the promotion gives "100%" discount on shipping to every order @@ -21,7 +21,7 @@ Feature: Reapplying promotion on cart change And there should be no shipping fee And there should be no discount - @ui + @ui @api Scenario: Receiving discount on shipping after shipping method change Given the store has "DHL" shipping method with "$10.00" fee And the store has "FedEx" shipping method with "$30.00" fee @@ -32,7 +32,7 @@ Feature: Reapplying promotion on cart change Then my cart total should be "$100.00" And my cart shipping should be for Free - @ui + @ui @api Scenario: Receiving discount after removing an item from the cart and then adding another one Given the store has a product "Symfony T-Shirt" priced at "$150.00" And the promotion gives "$10.00" discount to every order @@ -42,18 +42,18 @@ Feature: Reapplying promotion on cart change Then my cart total should be "$140.00" And my discount should be "-$10.00" - @ui + @ui @api Scenario: Not receiving discount when cart does not meet the required total value after removing an item Given the promotion gives "$10.00" discount to every order with items total at least "$120.00" And I have 2 products "PHP T-Shirt" in the cart - When I change "PHP T-Shirt" quantity to 1 + When I change product "PHP T-Shirt" quantity to 1 Then my cart total should be "$100.00" And there should be no discount - @ui + @ui @api Scenario: Not receiving discount when cart does not meet the required quantity after removing an item Given the promotion gives "$10.00" discount to every order with quantity at least 3 And I have 3 products "PHP T-Shirt" in the cart - When I change "PHP T-Shirt" quantity to 1 + When I change product "PHP T-Shirt" quantity to 1 Then my cart total should be "$100.00" And there should be no discount diff --git a/features/promotion/managing_catalog_promotions/editing_catalog_promotion.feature b/features/promotion/managing_catalog_promotions/editing_catalog_promotion.feature index b64fc5d110..a5b4845c36 100644 --- a/features/promotion/managing_catalog_promotions/editing_catalog_promotion.feature +++ b/features/promotion/managing_catalog_promotions/editing_catalog_promotion.feature @@ -70,7 +70,7 @@ Feature: Editing catalog promotion And this catalog promotion should be applied on "T-Shirt" product And this catalog promotion should not be applied on "PHP T-Shirt" variant - @api @ui @javascript + @api @ui Scenario: Editing catalog promotion action When I edit "Christmas sale" catalog promotion to have "40%" discount Then I should be notified that it has been successfully edited @@ -106,12 +106,12 @@ Feature: Editing catalog promotion And I save my changes Then I should be notified that not all channels are filled - @api @ui @javascript + @api @ui Scenario: Receiving error message after not filling percentage value for percentage discount When I want to modify a catalog promotion "Christmas sale" And I edit it to have empty amount of percentage discount And I save my changes - Then I should be notified that a discount amount should be a number and cannot be empty + Then I should be notified that the percentage amount should be a number and cannot be empty @api @ui @javascript Scenario: Editing catalog promotion action to be a percentage discount and not filling amount @@ -119,11 +119,11 @@ Feature: Editing catalog promotion When I want to modify a catalog promotion "Christmas sale" And I edit it to have empty amount of percentage discount And I save my changes - Then I should be notified that a discount amount should be a number and cannot be empty + Then I should be notified that the percentage amount should be a number and cannot be empty @api @ui @javascript Scenario: Editing catalog promotion action to be a fixed discount and not filling amount When I want to modify a catalog promotion "Christmas sale" And I edit it to have empty amount of fixed discount in the "United States" channel And I save my changes - Then I should be notified that a discount amount should be configured for at least one channel + Then I should be notified that the fixed amount should be a number and cannot be empty diff --git a/features/promotion/managing_catalog_promotions/sorting_catalog_promotions.feature b/features/promotion/managing_catalog_promotions/sorting_catalog_promotions.feature index 8a2c6d5e21..1d37a02957 100644 --- a/features/promotion/managing_catalog_promotions/sorting_catalog_promotions.feature +++ b/features/promotion/managing_catalog_promotions/sorting_catalog_promotions.feature @@ -16,69 +16,69 @@ Feature: Sorting listed catalog promotion And its priority is 2 And I am logged in as an administrator - @ui + @api @ui Scenario: Catalog promotions are sorted by ascending code by default When I browse catalog promotions Then I should see 3 catalog promotions on the list And the first catalog promotion should have code "a" - @ui + @api @ui Scenario: Changing the code sorting order to descending When I browse catalog promotions And I sort catalog promotions by descending code Then I should see 3 catalog promotions on the list And the first catalog promotion should have code "not-b" - @ui + @api @ui Scenario: Sorting catalog promotions by name in ascending order When I browse catalog promotions And I sort catalog promotions by ascending name Then I should see 3 catalog promotions on the list And the first catalog promotion should have code "a" - @ui + @api @ui Scenario: Sorting catalog promotion by name in descending order When I browse catalog promotions And I sort catalog promotions by descending name Then I should see 3 catalog promotions on the list And the first catalog promotion should have code "c" - @ui @no-postgres + @api @ui @no-postgres Scenario: Sorting catalog promotion by start date in ascending order When I browse catalog promotions And I sort catalog promotions by ascending "start date" Then I should see 3 catalog promotions on the list And the first catalog promotion should have code "not-b" - @ui @no-postgres + @api @ui @no-postgres Scenario: Sorting catalog promotion by start date in descending order When I browse catalog promotions And I sort catalog promotions by descending "start date" Then I should see 3 catalog promotions on the list And the first catalog promotion should have code "a" - @ui @no-postgres + @api @ui @no-postgres Scenario: Sorting catalog promotion by end date in ascending order When I browse catalog promotions And I sort catalog promotions by ascending "end date" Then I should see 3 catalog promotions on the list And the first catalog promotion should have code "a" - @ui @no-postgres + @api @ui @no-postgres Scenario: Sorting catalog promotion by end date in descending order When I browse catalog promotions And I sort catalog promotions by descending "end date" Then I should see 3 catalog promotions on the list And the first catalog promotion should have code "c" - @ui + @api @ui Scenario: Sorting catalog promotion by priority in ascending order When I browse catalog promotions And I sort catalog promotions by ascending priority Then I should see 3 catalog promotions on the list And the first catalog promotion should have code "not-b" - @ui + @api @ui Scenario: Sorting catalog promotion by priority in descending order When I browse catalog promotions And I sort catalog promotions by descending priority diff --git a/features/promotion/managing_catalog_promotions/validating_catalog_promotion_creation.feature b/features/promotion/managing_catalog_promotions/validating_catalog_promotion_creation.feature index 642e7cdf72..2fe43a03c6 100644 --- a/features/promotion/managing_catalog_promotions/validating_catalog_promotion_creation.feature +++ b/features/promotion/managing_catalog_promotions/validating_catalog_promotion_creation.feature @@ -86,7 +86,7 @@ Feature: Validating a catalog promotion creation And I add scope that applies on variants "PHP T-Shirt" variant and "Kotlin T-Shirt" variant And I add percentage discount action without amount configured And I try to add it - Then I should be notified that a discount amount should be a number and cannot be empty + Then I should be notified that the percentage amount should be a number and cannot be empty And there should be an empty list of catalog promotions @api @ui @mink:chromedriver @@ -111,7 +111,7 @@ Feature: Validating a catalog promotion creation And I add scope that applies on variants "PHP T-Shirt" variant and "Kotlin T-Shirt" variant And I add invalid percentage discount action with non number in amount And I try to add it - Then I should be notified that a discount amount should be a number and cannot be empty + Then I should be notified that the percentage amount should be a number and cannot be empty And there should be an empty list of catalog promotions @api @ui @mink:chromedriver @@ -123,9 +123,9 @@ Feature: Validating a catalog promotion creation And I specify its label as "Winter -50%" in "English (United States)" And I describe it as "This promotion gives a $10.00 discount on every product" in "English (United States)" And I add scope that applies on variants "PHP T-Shirt" variant and "Kotlin T-Shirt" variant - And I add fixed discount action without amount configured + And I add fixed discount action without amount configured for the "United States" channel And I try to add it - Then I should be notified that a discount amount should be configured for at least one channel + Then I should be notified that the fixed amount should be a number and cannot be empty And there should be an empty list of catalog promotions @api @@ -139,7 +139,7 @@ Feature: Validating a catalog promotion creation And I add scope that applies on variants "PHP T-Shirt" variant and "Kotlin T-Shirt" variant And I add invalid fixed discount action with non number in amount for the "United States" channel And I try to add it - Then I should be notified that a discount amount should be configured for at least one channel + Then I should be notified that the fixed amount should be a number and cannot be empty And there should be an empty list of catalog promotions @api diff --git a/features/promotion/managing_coupons/adding_coupon.feature b/features/promotion/managing_coupons/adding_coupon.feature index 8acc91cfb0..5f7c5ece9b 100644 --- a/features/promotion/managing_coupons/adding_coupon.feature +++ b/features/promotion/managing_coupons/adding_coupon.feature @@ -10,7 +10,7 @@ Feature: Adding a new coupon And it is coupon based promotion And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new coupon When I want to create a new coupon for this promotion And I specify its code as "SANTA2016" @@ -19,4 +19,4 @@ Feature: Adding a new coupon And I make it valid until "21.04.2017" And I add it Then I should be notified that it has been successfully created - And there should be coupon with code "SANTA2016" + And there should be a "Christmas sale" promotion with a coupon code "SANTA2016" diff --git a/features/promotion/managing_coupons/being_unable_to_generate_too_many_coupons_with_prefix_and_suffix.feature b/features/promotion/managing_coupons/being_unable_to_generate_too_many_coupons_with_prefix_and_suffix.feature index 4285c0f43a..b5a84c2ae0 100644 --- a/features/promotion/managing_coupons/being_unable_to_generate_too_many_coupons_with_prefix_and_suffix.feature +++ b/features/promotion/managing_coupons/being_unable_to_generate_too_many_coupons_with_prefix_and_suffix.feature @@ -14,7 +14,7 @@ Feature: Being unable to generate too many coupons with prefix and suffix And it is coupon based promotion And I am logged in as an administrator - @ui + @api @ui Scenario: Being unable to generate too many coupons with prefix Given I have generated 8 coupons for this promotion with code length 1 and prefix "CHRISTMAS_" When I want to generate new coupons for this promotion @@ -25,7 +25,7 @@ Feature: Being unable to generate too many coupons with prefix and suffix Then I should be notified that generating 2 coupons with code length equal to 1 is not possible And there should still be 8 coupons related to this promotion - @ui + @api @ui Scenario: Being unable to generate too many coupons with suffix Given I have generated 8 coupons for this promotion with code length 1 and suffix "_CHRISTMAS" When I want to generate new coupons for this promotion @@ -36,7 +36,7 @@ Feature: Being unable to generate too many coupons with prefix and suffix Then I should be notified that generating 2 coupons with code length equal to 1 is not possible And there should still be 8 coupons related to this promotion - @ui + @api @ui Scenario: Being unable to generate too many coupons with prefix and suffix Given I have generated 8 coupons for this promotion with code length 1, prefix "CHRISTMAS_" and suffix "_SALE" When I want to generate new coupons for this promotion diff --git a/features/promotion/managing_coupons/browsing_coupon.feature b/features/promotion/managing_coupons/browsing_coupons.feature similarity index 77% rename from features/promotion/managing_coupons/browsing_coupon.feature rename to features/promotion/managing_coupons/browsing_coupons.feature index c94595a8f7..e514a73c6b 100644 --- a/features/promotion/managing_coupons/browsing_coupon.feature +++ b/features/promotion/managing_coupons/browsing_coupons.feature @@ -9,8 +9,8 @@ Feature: Browsing promotion coupons And the store has promotion "Christmas sale" with coupon "SANTA2016" And I am logged in as an administrator - @ui - Scenario: Browsing coupons in store + @ui @api + Scenario: Browsing coupons of a promotion When I want to view all coupons of this promotion And there should be 1 coupon related to this promotion - And there should be coupon with code "SANTA2016" + And there should be a "Christmas sale" promotion with a coupon code "SANTA2016" diff --git a/features/promotion/managing_coupons/coupon_generate_validation.feature b/features/promotion/managing_coupons/coupon_generate_validation.feature index 325f901412..55c4b06ba1 100644 --- a/features/promotion/managing_coupons/coupon_generate_validation.feature +++ b/features/promotion/managing_coupons/coupon_generate_validation.feature @@ -10,7 +10,7 @@ Feature: Coupon generate instruction validation And it is coupon based promotion And I am logged in as an administrator - @ui + @api @ui Scenario: Trying to generate new coupons without specifying their amount When I want to generate new coupons for this promotion And I do not specify its amount @@ -21,7 +21,7 @@ Feature: Coupon generate instruction validation Then I should be notified that generate amount is required And there should be 0 coupon related to this promotion - @ui + @api @ui Scenario: Trying to generate new coupons without specifying their code length When I want to generate new coupons for this promotion And I do not specify their code length @@ -32,7 +32,7 @@ Feature: Coupon generate instruction validation Then I should be notified that generate code length is required And there should be 0 coupon related to this promotion - @ui + @api @ui Scenario: Trying to generate new coupons with code length impossible to generate When I want to generate new coupons for this promotion And I specify their code length as 50 @@ -43,7 +43,7 @@ Feature: Coupon generate instruction validation Then I should be notified that generate code length is out of range And there should be 0 coupon related to this promotion - @ui + @api @ui Scenario: Trying to generate new coupons with amount and code length impossible to generate When I want to generate new coupons for this promotion And I specify their code length as 1 diff --git a/features/promotion/managing_coupons/coupon_unique_code_validation.feature b/features/promotion/managing_coupons/coupon_unique_code_validation.feature index 4f2a1d82bf..9331d10ee5 100644 --- a/features/promotion/managing_coupons/coupon_unique_code_validation.feature +++ b/features/promotion/managing_coupons/coupon_unique_code_validation.feature @@ -9,7 +9,7 @@ Feature: Coupon unique code validation And the store has promotion "Christmas sale" with coupon "SANTA2016" And I am logged in as an administrator - @ui + @api @ui Scenario: Trying to add coupon with taken code When I want to create a new coupon for this promotion And I specify its code as "SANTA2016" diff --git a/features/promotion/managing_coupons/coupon_validation.feature b/features/promotion/managing_coupons/coupon_validation.feature index 282d5bd5e1..d1c4249809 100644 --- a/features/promotion/managing_coupons/coupon_validation.feature +++ b/features/promotion/managing_coupons/coupon_validation.feature @@ -10,7 +10,7 @@ Feature: Coupon validation And it is coupon based promotion And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add a new coupon without specifying its code When I want to create a new coupon for this promotion And I do not specify its code @@ -19,9 +19,9 @@ Feature: Coupon validation And I make it valid until "26.03.2017" And I try to add it Then I should be notified that code is required - And there should be 0 coupon related to this promotion + And there should be 0 coupons related to this promotion - @ui + @ui @api Scenario: Trying to add a new coupon with usage limit below one When I want to create a new coupon for this promotion And I specify its code as "SANTA2016" @@ -30,9 +30,9 @@ Feature: Coupon validation And I make it valid until "26.03.2017" And I try to add it Then I should be notified that coupon usage limit must be at least one - And there should be 0 coupon related to this promotion + And there should be 0 coupons related to this promotion - @ui + @ui @api Scenario: Trying to add a new coupon with per customer usage limit below one When I want to create a new coupon for this promotion And I specify its code as "SANTA2016" @@ -41,4 +41,27 @@ Feature: Coupon validation And I make it valid until "26.03.2017" And I try to add it Then I should be notified that coupon usage limit per customer must be at least one - And there should be 0 coupon related to this promotion + And there should be 0 coupons related to this promotion + + @api @no-ui + Scenario: Trying to add a new coupon with no promotion + When I want to create a new coupon + And I specify its code as "RANDOM" + And I limit its usage to 30 times + And I limit its per customer usage to 3 times + And I make it valid until "26.03.2017" + And I try to add it + Then I should be notified that promotion is required + And there should be no coupon with code "RANDOM" + + @api @no-ui + Scenario: Trying to add a new coupon for a non-coupon based promotion + Given there is a promotion "Flash sale" + When I want to create a new coupon for this promotion + And I specify its code as "FAST-50" + And I limit its usage to 50 times + And I limit its per customer usage to 1 time + And I make it valid until "26.03.2017" + And I try to add it + Then I should be notified that only coupon based promotions can have coupons + And there should be 0 coupons related to this promotion diff --git a/features/promotion/managing_coupons/deleting_coupon.feature b/features/promotion/managing_coupons/deleting_coupon.feature index efe196ac6f..08a737d410 100644 --- a/features/promotion/managing_coupons/deleting_coupon.feature +++ b/features/promotion/managing_coupons/deleting_coupon.feature @@ -9,7 +9,7 @@ Feature: Deleting a coupon And the store has promotion "Christmas sale" with coupon "SANTA2016" And I am logged in as an administrator - @domain @ui + @domain @ui @api Scenario: Deleted coupon should disappear from the registry When I delete "SANTA2016" coupon related to this promotion Then I should be notified that it has been successfully deleted diff --git a/features/promotion/managing_coupons/deleting_multiple_coupons.feature b/features/promotion/managing_coupons/deleting_multiple_coupons.feature index 7ebaefd0ed..dee4e09b77 100644 --- a/features/promotion/managing_coupons/deleting_multiple_coupons.feature +++ b/features/promotion/managing_coupons/deleting_multiple_coupons.feature @@ -10,7 +10,7 @@ Feature: Deleting multiple coupons And this promotion has "SANTA1", "SANTA2" and "SANTA3" coupons And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple coupons at once When I browse coupons of this promotion And I check the "SANTA1" coupon diff --git a/features/promotion/managing_coupons/editing_coupon.feature b/features/promotion/managing_coupons/editing_coupon.feature index 0bbe347fdd..c1e4afe187 100644 --- a/features/promotion/managing_coupons/editing_coupon.feature +++ b/features/promotion/managing_coupons/editing_coupon.feature @@ -9,15 +9,15 @@ Feature: Editing promotion coupon And the store has promotion "Christmas sale" with coupon "SANTA2016" And I am logged in as an administrator - @ui + @ui @api Scenario: Changing coupon expires date When I want to modify the "SANTA2016" coupon for this promotion - And I change expires date to "21.05.2019" + And I change its expiration date to "21.05.2019" And I save my changes Then I should be notified that it has been successfully edited And this coupon should be valid until "21.05.2019" - @ui + @ui @api Scenario: Changing coupons usage limit When I want to modify the "SANTA2016" coupon for this promotion And I change its usage limit to 50 @@ -25,7 +25,7 @@ Feature: Editing promotion coupon Then I should be notified that it has been successfully edited And this coupon should have 50 usage limit - @ui + @ui @api Scenario: Changing coupons per customer usage limit When I want to modify the "SANTA2016" coupon for this promotion And I change its per customer usage limit to 20 @@ -33,7 +33,15 @@ Feature: Editing promotion coupon Then I should be notified that it has been successfully edited And this coupon should have 20 per customer usage limit - @ui - Scenario: Seeing a disabled code field when editing a coupon + @ui @api + Scenario: Changing whether it can be reused from cancelled orders When I want to modify the "SANTA2016" coupon for this promotion - Then the code field should be disabled + And I make it not reusable from cancelled orders + And I save my changes + Then I should be notified that it has been successfully edited + And this coupon should not be reusable from cancelled orders + + @ui @api + Scenario: Being unable to change code of promotion coupon + When I want to modify the "SANTA2016" coupon for this promotion + Then I should not be able to edit its code diff --git a/features/promotion/managing_coupons/generating_coupon.feature b/features/promotion/managing_coupons/generating_coupon.feature index c5db05f7c1..5fa2812afa 100644 --- a/features/promotion/managing_coupons/generating_coupon.feature +++ b/features/promotion/managing_coupons/generating_coupon.feature @@ -10,7 +10,7 @@ Feature: Generating new coupons And it is coupon based promotion And I am logged in as an administrator - @ui + @api @ui Scenario: Generating a new coupons When I want to generate new coupons for this promotion And I choose the amount of 5 coupons to be generated @@ -21,7 +21,7 @@ Feature: Generating new coupons Then I should be notified that they have been successfully generated And there should be 5 coupons related to this promotion - @ui + @api @ui Scenario: Generating new coupons without expiration date When I want to generate new coupons for this promotion And I choose the amount of 5 coupons to be generated @@ -31,11 +31,11 @@ Feature: Generating new coupons Then I should be notified that they have been successfully generated And there should be 5 coupons related to this promotion - @ui + @api @ui Scenario: Generating new coupons with a large long code length value When I want to generate new coupons for this promotion And I choose the amount of 10 coupons to be generated And I specify their code length as 40 - And I generate it + And I generate these coupons Then I should be notified that they have been successfully generated And there should be 10 coupons related to this promotion diff --git a/features/promotion/managing_coupons/generating_coupon_with_prefix_and_suffix.feature b/features/promotion/managing_coupons/generating_coupon_with_prefix_and_suffix.feature index f009726f7e..1918ed8e36 100644 --- a/features/promotion/managing_coupons/generating_coupon_with_prefix_and_suffix.feature +++ b/features/promotion/managing_coupons/generating_coupon_with_prefix_and_suffix.feature @@ -10,7 +10,7 @@ Feature: Generating new coupons with prefix and suffix And it is coupon based promotion And I am logged in as an administrator - @ui + @api @ui Scenario: Generating new coupons with prefix When I want to generate new coupons for this promotion And I choose the amount of 5 coupons to be generated @@ -21,7 +21,7 @@ Feature: Generating new coupons with prefix and suffix And there should be 5 coupons related to this promotion And all of the coupon codes should be prefixed with "CHRISTMAS_" - @ui + @api @ui Scenario: Generating new coupons with suffix When I want to generate new coupons for this promotion And I choose the amount of 5 coupons to be generated @@ -32,7 +32,7 @@ Feature: Generating new coupons with prefix and suffix And there should be 5 coupons related to this promotion And all of the coupon codes should be suffixed with "_CHRISTMAS" - @ui + @api @ui Scenario: Generating new coupons with prefix and suffix When I want to generate new coupons for this promotion And I choose the amount of 5 coupons to be generated diff --git a/features/promotion/managing_coupons/preventing_deletion_of_coupons_in_use.feature b/features/promotion/managing_coupons/preventing_deletion_of_coupons_in_use.feature index 527ffe64b5..77d37799c1 100644 --- a/features/promotion/managing_coupons/preventing_deletion_of_coupons_in_use.feature +++ b/features/promotion/managing_coupons/preventing_deletion_of_coupons_in_use.feature @@ -15,7 +15,7 @@ Feature: Not being able to delete a coupon which is in use And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @domain @ui + @domain @ui @api Scenario: Being unable to delete a used coupon When I try to delete "SANTA2016" coupon related to this promotion Then I should be notified that it is in use and cannot be deleted diff --git a/features/promotion/managing_coupons/sorting_coupons.feature b/features/promotion/managing_coupons/sorting_coupons.feature index b3c00f7809..e1eb8ba9d7 100644 --- a/features/promotion/managing_coupons/sorting_coupons.feature +++ b/features/promotion/managing_coupons/sorting_coupons.feature @@ -19,69 +19,69 @@ Feature: Sorting listed coupons And this coupon expires on "20-02-2023" And I am logged in as an administrator - @ui + @ui @api Scenario: Coupons are sorted by descending number of uses by default When I want to view all coupons of this promotion Then I should see 3 coupons on the list And the first coupon should have code "Y" - @ui + @ui @api Scenario: Changing the number of uses sorting order to ascending Given I am browsing coupons of this promotion When I sort coupons by ascending number of uses Then I should see 3 coupons on the list And the first coupon should have code "X" - @ui + @ui @api Scenario: Sorting coupons by code in descending order Given I am browsing coupons of this promotion When I sort coupons by descending code Then I should see 3 coupons on the list And the first coupon should have code "Z" - @ui + @ui @api Scenario: Sorting coupons by code in ascending order Given I am browsing coupons of this promotion When I sort coupons by ascending code Then I should see 3 coupons on the list And the first coupon should have code "X" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by usage limit in descending order Given I am browsing coupons of this promotion When I sort coupons by descending usage limit Then I should see 3 coupons on the list And the first coupon should have code "X" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by usage limit in ascending order Given I am browsing coupons of this promotion When I sort coupons by ascending usage limit Then I should see 3 coupons on the list And the first coupon should have code "Z" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by usage limit per customer in descending order Given I am browsing coupons of this promotion When I sort coupons by descending usage limit per customer Then I should see 3 coupons on the list And the first coupon should have code "X" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by usage limit per customer in ascending order Given I am browsing coupons of this promotion When I sort coupons by ascending usage limit per customer Then I should see 3 coupons on the list And the first coupon should have code "Y" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by expiration date in descending order Given I am browsing coupons of this promotion When I sort coupons by descending expiration date Then I should see 3 coupons on the list And the first coupon should have code "Z" - @ui @no-postgres + @ui @no-postgres @api Scenario: Sorting coupons by expiration date in ascending order Given I am browsing coupons of this promotion When I sort coupons by ascending expiration date diff --git a/features/promotion/managing_promotions/accessing_the_coupons_management_from_the_promotion_page.feature b/features/promotion/managing_promotions/accessing_the_coupons_management_from_the_promotion_page.feature index 7f798c5c39..64bd6d998f 100644 --- a/features/promotion/managing_promotions/accessing_the_coupons_management_from_the_promotion_page.feature +++ b/features/promotion/managing_promotions/accessing_the_coupons_management_from_the_promotion_page.feature @@ -10,13 +10,13 @@ Feature: Accessing the coupons management from the promotion page And it is a coupon based promotion And I am logged in as an administrator - @ui + @ui @no-api Scenario: Being able to manage promotion's coupons When I modify a "Christmas sale" promotion And I want to manage this promotion coupons Then I should be on this promotion's coupons management page - @ui + @ui @no-api Scenario: Add new promotion and not being able to manage promotion's coupons When I create a new promotion And I specify its code as "FULL_METAL_PROMOTION" diff --git a/features/promotion/managing_promotions/adding_promotion.feature b/features/promotion/managing_promotions/adding_promotion.feature index 5279b27e9f..8ca1c91bea 100644 --- a/features/promotion/managing_promotions/adding_promotion.feature +++ b/features/promotion/managing_promotions/adding_promotion.feature @@ -8,7 +8,7 @@ Feature: Adding a new promotion Given the store operates on a single channel in "United States" And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new promotion When I want to create a new promotion And I specify its code as "FULL_METAL_PROMOTION" @@ -17,7 +17,7 @@ Feature: Adding a new promotion Then I should be notified that it has been successfully created And the "Full metal promotion" promotion should appear in the registry - @ui + @api @ui Scenario: Adding a new promotion with usage limit When I want to create a new promotion And I specify its code as "FULL_METAL_PROMOTION" @@ -27,17 +27,17 @@ Feature: Adding a new promotion Then I should be notified that it has been successfully created And the "Full metal promotion" promotion should be available to be used only 50 times - @ui + @api @ui Scenario: Adding a new exclusive promotion When I want to create a new promotion And I specify its code as "FULL_METAL_PROMOTION" And I name it "Full metal promotion" - And I make it exclusive + And I set it as exclusive And I add it Then I should be notified that it has been successfully created And the "Full metal promotion" promotion should be exclusive - @ui + @api @ui Scenario: Adding a new coupon based promotion When I want to create a new promotion And I specify its code as "FULL_METAL_PROMOTION" @@ -47,7 +47,7 @@ Feature: Adding a new promotion Then I should be notified that it has been successfully created And the "Full metal promotion" promotion should be coupon based - @ui + @api @ui Scenario: Adding a new channels promotion When I want to create a new promotion And I specify its code as "FULL_METAL_PROMOTION" @@ -57,7 +57,7 @@ Feature: Adding a new promotion Then I should be notified that it has been successfully created And the "Full metal promotion" promotion should be applicable for the "United States" channel - @ui + @api @ui Scenario: Adding a promotion with start and end date When I want to create a new promotion And I specify its code as "FULL_METAL_PROMOTION" diff --git a/features/promotion/managing_promotions/adding_promotion_in_different_languages.feature b/features/promotion/managing_promotions/adding_promotion_in_different_languages.feature new file mode 100644 index 0000000000..65ae9d7bc3 --- /dev/null +++ b/features/promotion/managing_promotions/adding_promotion_in_different_languages.feature @@ -0,0 +1,21 @@ +@managing_promotions +Feature: Adding promotion in different languages + In order to see the label of promotion in a specific language + As an Administrator + I want to be able to add a promotion and specify labels in different languages + + Background: + Given the store operates on a single channel in "United States" + And that channel allows to shop using "English (United States)" and "Polish (Poland)" locales + And I am logged in as an administrator + + @api @ui @javascript + Scenario: Adding a promotion with a label in a different language + When I want to create a new promotion + And I specify its code as "FULL_METAL_PROMOTION" + And I name it "Full metal promotion" + And I specify its label as "W pełni metalowa promocja" in "Polish (Poland)" locale + And I add it + Then I should be notified that it has been successfully created + And the "Full metal promotion" promotion should appear in the registry + And the "Full metal promotion" promotion should have a label "W pełni metalowa promocja" in "Polish (Poland)" locale diff --git a/features/promotion/managing_promotions/adding_promotion_with_action.feature b/features/promotion/managing_promotions/adding_promotion_with_action.feature index e1071f4aca..9aed8f5060 100644 --- a/features/promotion/managing_promotions/adding_promotion_with_action.feature +++ b/features/promotion/managing_promotions/adding_promotion_with_action.feature @@ -8,7 +8,7 @@ Feature: Adding a new promotion with action Given the store operates on a single channel in "United States" And I am logged in as an administrator - @ui @javascript + @api @ui @javascript Scenario: Adding a new promotion with fixed discount When I want to create a new promotion And I specify its code as "10_for_all_products" @@ -18,12 +18,12 @@ Feature: Adding a new promotion with action Then I should be notified that it has been successfully created And the "$10.00 for all products!" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a promotion with item percentage discount When I want to create a new promotion And I specify its code as "promotion_for_all_product_items" And I name it "-20% for all product items!" - And I add the "Item percentage discount" action configured with a percentage value of 20% for "United States" channel + And I add the "Item percentage discount" action configured with a percentage value of "20%" for "United States" channel And I add it Then I should be notified that it has been successfully created And it should have "20.00%" of item percentage discount configured for "United States" channel diff --git a/features/promotion/managing_promotions/adding_promotion_with_action_in_different_channels.feature b/features/promotion/managing_promotions/adding_promotion_with_action_in_different_channels.feature index 3320a8b30c..da704d642d 100644 --- a/features/promotion/managing_promotions/adding_promotion_with_action_in_different_channels.feature +++ b/features/promotion/managing_promotions/adding_promotion_with_action_in_different_channels.feature @@ -9,13 +9,13 @@ Feature: Adding a new promotion with action configured in different channels And the store also operates on another channel named "Web-GB" in "GBP" currency And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding a new promotion with item fixed discount When I want to create a new promotion And I specify its code as "20_for_all_products" And I name it "Item fixed discount for all products!" - And I add the "Item fixed discount" action configured with amount of "$10" for "United States" channel + And I add the "Item fixed discount" action configured with amount of "$10.00" for "United States" channel And it is also configured with amount of "£16.00" for "Web-GB" channel And I add it Then I should be notified that it has been successfully created - And the "Fixed discount for all products!" promotion should appear in the registry + And the "Item fixed discount for all products!" promotion should appear in the registry diff --git a/features/promotion/managing_promotions/adding_promotion_with_filter.feature b/features/promotion/managing_promotions/adding_promotion_with_filter.feature index d0dbac22be..62e7e2388c 100644 --- a/features/promotion/managing_promotions/adding_promotion_with_filter.feature +++ b/features/promotion/managing_promotions/adding_promotion_with_filter.feature @@ -8,93 +8,93 @@ Feature: Adding promotion with filter Given the store operates on a single channel in "United States" And I am logged in as an administrator - @ui @mink:chromedriver + @api @ui @mink:chromedriver Scenario: Adding a promotion with item fixed discount only for products over 10 When I want to create a new promotion And I specify its code as "10_for_all_products_over_10" And I name it "$10 discount for all products over $10!" - And I add the "Item fixed discount" action configured with amount of "$10" for "United States" channel - And I specify that on "United States" channel this action should be applied to items with price greater then "$10" + And I add the "Item fixed discount" action configured with amount of "$10.00" for "United States" channel + And I specify that on "United States" channel this action should be applied to items with price greater than "$10.00" And I add it Then I should be notified that it has been successfully created And the "$10 discount for all products over $10!" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a promotion with item fixed discount only for products between 10 and 100 When I want to create a new promotion And I specify its code as "10_for_all_products_over_10" And I name it "$10 discount for (almost) all products!" - And I add the "Item fixed discount" action configured with amount of "$10" for "United States" channel - And I specify that on "United States" channel this action should be applied to items with price between "$10" and "$100" + And I add the "Item fixed discount" action configured with amount of "$10.00" for "United States" channel + And I specify that on "United States" channel this action should be applied to items with price between "$10.00" and "$100.00" And I add it Then I should be notified that it has been successfully created And the "$10 discount for (almost) all products!" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a promotion with fixed discount for all t-shirts Given the store classifies its products as "T-Shirts" and "Mugs" When I want to create a new promotion And I specify its code as "10_for_all_t_shirts" And I name it "$10 discount for all T-Shirts!" - And I add the "Item fixed discount" action configured with amount of "$10" for "United States" channel - And I specify that this action should be applied to items from "T-Shirt" category + And I add the "Item fixed discount" action configured with amount of "$10.00" for "United States" channel + And I specify that this action should be applied to items from "T-Shirts" category And I add it Then I should be notified that it has been successfully created And the "$10 discount for all T-Shirts!" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a promotion with fixed discount for PHP T-Shirt Given the store has a product "PHP T-Shirt" priced at "$100.00" When I want to create a new promotion And I specify its code as "10_for_php_t_shirt" And I name it "$10 discount for PHP T-Shirts!" - And I add the "Item fixed discount" action configured with amount of "$10" for "United States" channel + And I add the "Item fixed discount" action configured with amount of "$10.00" for "United States" channel And I specify that this action should be applied to the "PHP T-Shirt" product And I add it Then I should be notified that it has been successfully created And the "$10 discount for PHP T-Shirts!" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a promotion with item percentage discount only for products over 10 When I want to create a new promotion And I specify its code as "10_for_all_products_over_10" And I name it "$10 discount for all products over $10!" - And I add the "Item percentage discount" action configured with a percentage value of 10% for "United States" channel - And I specify that on "United States" channel this action should be applied to items with price greater then "$10" + And I add the "Item percentage discount" action configured with a percentage value of "10%" for "United States" channel + And I specify that on "United States" channel this action should be applied to items with price greater than "$10.00" And I add it Then I should be notified that it has been successfully created And the "$10 discount for all products over $10!" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a promotion with item percentage discount only for products between 10 and 100 When I want to create a new promotion And I specify its code as "10_for_all_products_over_10" And I name it "$10 discount for (almost) all products!" - And I add the "Item percentage discount" action configured with a percentage value of 10% for "United States" channel - And I specify that on "United States" channel this action should be applied to items with price between "$10" and "$100" + And I add the "Item percentage discount" action configured with a percentage value of "10%" for "United States" channel + And I specify that on "United States" channel this action should be applied to items with price between "$10.00" and "$100.00" And I add it Then I should be notified that it has been successfully created And the "$10 discount for (almost) all products!" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a promotion with 10% percentage discount for all t-shirts Given the store classifies its products as "T-Shirts" and "Mugs" When I want to create a new promotion And I specify its code as "10_for_all_t_shirts" And I name it "$10 discount for all T-Shirts!" - And I add the "Item percentage discount" action configured with a percentage value of 10% for "United States" channel - And I specify that this action should be applied to items from "T-Shirt" category + And I add the "Item percentage discount" action configured with a percentage value of "10%" for "United States" channel + And I specify that this action should be applied to items from "T-Shirts" category And I add it Then I should be notified that it has been successfully created And the "$10 discount for all T-Shirts!" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a promotion with 10% percentage discount for PHP T-Shirt Given the store has a product "PHP T-Shirt" priced at "$100.00" When I want to create a new promotion And I specify its code as "10_for_php_t_shirt" And I name it "10% discount for PHP T-Shirts!" - And I add the "Item percentage discount" action configured with a percentage value of 10% for "United States" channel + And I add the "Item percentage discount" action configured with a percentage value of "10%" for "United States" channel And I specify that this action should be applied to the "PHP T-Shirt" product And I add it Then I should be notified that it has been successfully created diff --git a/features/promotion/managing_promotions/adding_promotion_with_rule.feature b/features/promotion/managing_promotions/adding_promotion_with_rule.feature index 997223a7fd..8014a1e5f1 100644 --- a/features/promotion/managing_promotions/adding_promotion_with_rule.feature +++ b/features/promotion/managing_promotions/adding_promotion_with_rule.feature @@ -9,27 +9,27 @@ Feature: Adding a new promotion with rule And the store classifies its products as "T-Shirts" and "Mugs" And I am logged in as an administrator - @ui @javascript + @api @ui @javascript Scenario: Adding a new promotion with taxon rule When I want to create a new promotion And I specify its code as "HOLIDAY_SALE" And I name it "Holiday sale" - And I add the "Has at least one from taxons" rule configured with "T-Shirts" and "Mugs" + And I add the "Has at least one from taxons" rule configured with "T-Shirts" taxon and "Mugs" taxon And I add it Then I should be notified that it has been successfully created And the "Holiday sale" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a new promotion with total price of items from taxon rule When I want to create a new promotion And I specify its code as "100_MUGS_PROMOTION" And I name it "100 Mugs promotion" - And I add the "Total price of items from taxon" rule configured with "Mugs" taxon and $100 amount for "United States" channel + And I add the "Total price of items from taxon" rule configured with "Mugs" taxon and "$100.00" amount for "United States" channel And I add it Then I should be notified that it has been successfully created And the "100 Mugs promotion" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a new promotion with contains product rule Given the store has a product "PHP T-Shirt" priced at "$100.00" When I want to create a new promotion @@ -40,7 +40,7 @@ Feature: Adding a new promotion with rule Then I should be notified that it has been successfully created And the "PHP T-Shirt promotion" promotion should appear in the registry - @ui @javascript + @api @ui @javascript Scenario: Adding a new group based promotion Given the store has a customer group "Wholesale" When I want to create a new promotion @@ -51,7 +51,7 @@ Feature: Adding a new promotion with rule Then I should be notified that it has been successfully created And the "Wholesale promotion" promotion should appear in the registry - @ui @javascript + @ui @javascript @no-api Scenario: Adding a new promotion of default type with one action When I want to create a new promotion And I add a new rule diff --git a/features/promotion/managing_promotions/adding_promotion_with_rule_in_different_channels.feature b/features/promotion/managing_promotions/adding_promotion_with_rule_in_different_channels.feature index fc6c071fd1..5c572c6738 100644 --- a/features/promotion/managing_promotions/adding_promotion_with_rule_in_different_channels.feature +++ b/features/promotion/managing_promotions/adding_promotion_with_rule_in_different_channels.feature @@ -9,12 +9,12 @@ Feature: Adding a new promotion with rule configured in different channels And the store operates on a channel named "Web-GB" in "GBP" currency And I am logged in as an administrator - @ui @mink:chromedriver - Scenario: Adding a new promotion with total price of items from taxon rule + @api @ui @mink:chromedriver + Scenario: Adding a new promotion with total price of items When I want to create a new promotion And I specify its code as "100_IN_EVERY_CURRENCY" And I name it "100 in every currency" - And I add the "Item total" rule configured with €100 amount for "United States" channel and £100 amount for "Web-GB" channel + And I add the "Item total" rule configured with "€100.00" amount for "United States" channel and "£100.00" amount for "Web-GB" channel And I add it Then I should be notified that it has been successfully created And the "100 in every currency" promotion should appear in the registry diff --git a/features/promotion/managing_promotions/archiving_promotions.feature b/features/promotion/managing_promotions/archiving_promotions.feature new file mode 100644 index 0000000000..2ffb43e42d --- /dev/null +++ b/features/promotion/managing_promotions/archiving_promotions.feature @@ -0,0 +1,41 @@ +@managing_promotions +Feature: Archiving promotions + In order to hide no longer available promotions from the list and customers' use + As an Administrator + I want to archive promotions + + Background: + Given the store operates on a single channel in "United States" + And there is a promotion "Christmas sale" + And there is also a promotion "New Year sale" + And I am logged in as an administrator + + @api @ui + Scenario: Archiving a promotion + When I browse promotions + And I archive the "Christmas sale" promotion + Then I should see a single promotion in the list + And I should see the promotion "New Year sale" in the list + + @domain + Scenario: Archiving a promotion does not remove it from the database + When I archive the "Christmas sale" promotion + Then the promotion "Christmas sale" should still exist in the registry + + @api @ui + Scenario: Seeing only archived promotions + Given the promotion "Christmas sale" is archived + When I browse promotions + And I filter archival promotions + Then I should see a single promotion in the list + And I should see the promotion "Christmas sale" in the list + And I should not see the promotion "New Year sale" in the list + + @api @ui + Scenario: Restoring an archival promotion + Given the promotion "Christmas sale" is archived + When I browse promotions + And I filter archival promotions + And I restore the "Christmas sale" promotion + Then I should be viewing non archival promotions + And I should see 2 promotions on the list diff --git a/features/promotion/managing_promotions/browsing_promotions.feature b/features/promotion/managing_promotions/browsing_promotions.feature index d96e582a35..22161f20b5 100644 --- a/features/promotion/managing_promotions/browsing_promotions.feature +++ b/features/promotion/managing_promotions/browsing_promotions.feature @@ -9,13 +9,13 @@ Feature: Browsing promotions And there is a promotion "Basic promotion" And I am logged in as an administrator - @ui @api + @api @ui Scenario: Browsing promotions When I want to browse promotions Then I should see a single promotion in the list And the "Basic promotion" promotion should exist in the registry - @ui @api + @api @ui Scenario: Browsing manage button for coupon based promotion Given the store has promotion "Christmas sale" with coupon "Santa's gift" When I want to browse promotions diff --git a/features/promotion/managing_promotions/deleting_multiple_promotions.feature b/features/promotion/managing_promotions/deleting_multiple_promotions.feature index 2be7e25a5c..b3f907f5ff 100644 --- a/features/promotion/managing_promotions/deleting_multiple_promotions.feature +++ b/features/promotion/managing_promotions/deleting_multiple_promotions.feature @@ -11,7 +11,7 @@ Feature: Deleting multiple promotions And there is also a promotion "Easter sale" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple promotions at once When I browse promotions And I check the "Christmas sale" promotion diff --git a/features/promotion/managing_promotions/deleting_promotion.feature b/features/promotion/managing_promotions/deleting_promotion.feature index 32abb00d58..528917c402 100644 --- a/features/promotion/managing_promotions/deleting_promotion.feature +++ b/features/promotion/managing_promotions/deleting_promotion.feature @@ -9,7 +9,7 @@ Feature: Deleting a promotion And there is a promotion "Christmas sale" And I am logged in as an administrator - @domain @ui @api + @domain @api @ui Scenario: Deleted promotion should disappear from the registry When I delete a "Christmas sale" promotion Then I should be notified that it has been successfully deleted diff --git a/features/promotion/managing_promotions/editing_promotion.feature b/features/promotion/managing_promotions/editing_promotion.feature index 6b4b3955ab..2eabd044bd 100644 --- a/features/promotion/managing_promotions/editing_promotion.feature +++ b/features/promotion/managing_promotions/editing_promotion.feature @@ -10,12 +10,12 @@ Feature: Editing promotion And there is a promotion "Holiday sale" with priority 1 And I am logged in as an administrator - @ui - Scenario: Seeing disabled code field when editing promotion + @api @ui + Scenario: Being unable to change code of promotion When I want to modify a "Christmas sale" promotion - Then the code field should be disabled + Then I should not be able to edit its code - @ui + @api @ui Scenario: Editing promotions usage limit When I want to modify a "Christmas sale" promotion And I set its usage limit to 50 @@ -23,15 +23,15 @@ Feature: Editing promotion Then I should be notified that it has been successfully edited And the "Christmas sale" promotion should be available to be used only 50 times - @ui + @api @ui Scenario: Editing promotion exclusiveness When I want to modify a "Christmas sale" promotion - And I make it exclusive + And I set it as exclusive And I save my changes Then I should be notified that it has been successfully edited And the "Christmas sale" promotion should be exclusive - @ui + @api @ui Scenario: Editing promotions coupon based option When I want to modify a "Christmas sale" promotion And I make it coupon based @@ -39,7 +39,7 @@ Feature: Editing promotion Then I should be notified that it has been successfully edited And the "Christmas sale" promotion should be coupon based - @ui + @api @ui Scenario: Editing promotions channels When I want to modify a "Christmas sale" promotion And I make it applicable for the "United States" channel @@ -47,7 +47,7 @@ Feature: Editing promotion Then I should be notified that it has been successfully edited And the "Christmas sale" promotion should be applicable for the "United States" channel - @ui + @api @ui Scenario: Editing a promotion with start and end date When I want to modify a "Christmas sale" promotion And I make it available from "12.12.2017" to "24.12.2017" @@ -55,16 +55,24 @@ Feature: Editing promotion Then I should be notified that it has been successfully edited And the "Christmas sale" promotion should be available from "12.12.2017" to "24.12.2017" - @ui + @api @ui Scenario: Editing promotion after adding a new channel Given this promotion gives "$10.00" discount to every order When the store also operates on another channel named "EU-WEB" Then I should be able to modify a "Christmas sale" promotion - @ui + @ui @no-api Scenario: Remove priority from existing promotion When I want to modify a "Christmas sale" promotion And I remove its priority And I save my changes Then I should be notified that it has been successfully edited And the "Christmas sale" promotion should have priority 1 + + @api @ui + Scenario: Setting promotion to the lowest priority + When I want to modify a "Christmas sale" promotion + And I set its priority to "-1" + And I save my changes + Then I should be notified that it has been successfully edited + And the "Christmas sale" promotion should have priority 1 diff --git a/features/promotion/managing_promotions/filtering_promotions_by_coupon_code.feature b/features/promotion/managing_promotions/filtering_promotions_by_coupon_code.feature index e7c3c5d469..7b8eb87ea4 100644 --- a/features/promotion/managing_promotions/filtering_promotions_by_coupon_code.feature +++ b/features/promotion/managing_promotions/filtering_promotions_by_coupon_code.feature @@ -11,7 +11,7 @@ Feature: Filtering promotions by coupon code And the store has promotion "Christmas sale" with coupon "MAGIC" And I am logged in as an administrator - @ui + @api @ui Scenario: Filtering promotions by coupon code When I want to browse promotions And I filter promotions by coupon code equal "MAGIC" diff --git a/features/promotion/managing_promotions/prevent_from_removing_products_that_are_used_in_promotion_rules.feature b/features/promotion/managing_promotions/prevent_from_removing_products_that_are_used_in_promotion_rules.feature index 55ba66e15b..7454f04ec8 100644 --- a/features/promotion/managing_promotions/prevent_from_removing_products_that_are_used_in_promotion_rules.feature +++ b/features/promotion/managing_promotions/prevent_from_removing_products_that_are_used_in_promotion_rules.feature @@ -9,7 +9,7 @@ Feature: Preventing from removing products that are used in promotion rules And the store has "Mug" and "Cup" products And I am logged in as an administrator - @ui @api + @api @ui Scenario: Being prevented from removing a product that is in use by a promotion rule Given there is a promotion "Christmas sale" with "Contains product" rule with product "Mug" When I try to delete the "Mug" product diff --git a/features/promotion/managing_promotions/prevent_from_removing_taxons_that_are_used_in_promotion_rules.feature b/features/promotion/managing_promotions/prevent_from_removing_taxons_that_are_used_in_promotion_rules.feature index 1f320e7687..842506bcfb 100644 --- a/features/promotion/managing_promotions/prevent_from_removing_taxons_that_are_used_in_promotion_rules.feature +++ b/features/promotion/managing_promotions/prevent_from_removing_taxons_that_are_used_in_promotion_rules.feature @@ -9,7 +9,7 @@ Feature: Preventing from removing taxons that are used in promotion rules And the store has "Mugs" taxonomy And I am logged in as an administrator - @ui @mink:chromedriver + @api @ui @mink:chromedriver Scenario: Being prevented from removing taxon that is in use by a promotion rule Given there is a promotion "Christmas sale" with "Total price of items from taxon" rule configured with "Mugs" taxon and $100 amount for "United States" channel When I try to delete taxon named "Mugs" diff --git a/features/promotion/managing_promotions/preventing_deletion_of_promotions_in_use.feature b/features/promotion/managing_promotions/preventing_deletion_of_promotions_in_use.feature index a6b27ca83a..a250a8f897 100644 --- a/features/promotion/managing_promotions/preventing_deletion_of_promotions_in_use.feature +++ b/features/promotion/managing_promotions/preventing_deletion_of_promotions_in_use.feature @@ -16,7 +16,7 @@ Feature: Prevent deletion of promotions applied to order And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @domain @ui + @domain @api @ui Scenario: Being unable to delete a promotion that was applied to an order When I try to delete a "Christmas sale" promotion Then I should be notified that it is in use and cannot be deleted diff --git a/features/promotion/managing_promotions/promotion_unique_code_validation.feature b/features/promotion/managing_promotions/promotion_unique_code_validation.feature index e76fc88510..7f290f63f0 100644 --- a/features/promotion/managing_promotions/promotion_unique_code_validation.feature +++ b/features/promotion/managing_promotions/promotion_unique_code_validation.feature @@ -9,7 +9,7 @@ Feature: Promotion unique code validation And there is a promotion "No-VAT promotion" identified by "NO_VAT" code And I am logged in as an administrator - @ui + @api @ui Scenario: Trying to add promotion with taken code When I want to create a new promotion And I specify its code as "NO_VAT" diff --git a/features/promotion/managing_promotions/promotion_validation.feature b/features/promotion/managing_promotions/promotion_validation.feature index 80aa9315c0..48fdee888a 100644 --- a/features/promotion/managing_promotions/promotion_validation.feature +++ b/features/promotion/managing_promotions/promotion_validation.feature @@ -6,9 +6,10 @@ Feature: Promotion validation Background: Given the store operates on a single channel in "United States" + And that channel allows to shop using "English (United States)" and "Polish (Poland)" locales And I am logged in as an administrator - @ui + @api @ui Scenario: Trying to add a new promotion without specifying its code When I want to create a new promotion And I name it "No-VAT promotion" @@ -17,7 +18,7 @@ Feature: Promotion validation Then I should be notified that code is required And promotion with name "No-VAT promotion" should not be added - @ui + @api @ui Scenario: Trying to add a new promotion without specifying its name When I want to create a new promotion And I specify its code as "no_vat_promotion" @@ -26,16 +27,16 @@ Feature: Promotion validation Then I should be notified that name is required And promotion with code "no_vat_promotion" should not be added - @ui + @api @ui Scenario: Adding a promotion with start date set up after end date When I want to create a new promotion And I specify its code as "FULL_METAL_PROMOTION" And I name it "Full metal promotion" And I make it available from "24.12.2017" to "12.12.2017" And I try to add it - Then I should be notified that promotion cannot end before it start + Then I should be notified that promotion cannot end before it starts - @ui + @api @ui Scenario: Trying to remove name from existing promotion Given there is a promotion "Christmas sale" When I want to modify this promotion @@ -44,15 +45,23 @@ Feature: Promotion validation Then I should be notified that name is required And this promotion should still be named "Christmas sale" - @ui + @api @ui Scenario: Trying to add start later then end date for existing promotion Given there is a promotion "Christmas sale" When I want to modify this promotion And I make it available from "24.12.2017" to "12.12.2017" And I try to save my changes - Then I should be notified that promotion cannot end before it start + Then I should be notified that promotion cannot end before it starts - @ui @javascript + @api @ui @mink:chromedriver + Scenario: Adding a promotion with label exceeding 255 characters + Given there is a promotion "Christmas sale" + When I want to modify this promotion + And I replace its label with a string exceeding the limit in "Polish (Poland)" locale + And I try to save my changes + Then I should be notified that promotion label in "Polish (Poland)" locale is too long + + @api @ui @javascript Scenario: Trying to add a new promotion without specifying a order percentage discount When I want to create a new promotion And I specify its code as "christmas_sale" @@ -62,32 +71,32 @@ Feature: Promotion validation Then I should be notified that this value should not be blank And promotion with name "Christmas sale" should not be added - @ui @javascript + @api @ui @javascript Scenario: Trying to add a new promotion without specifying an item percentage discount When I want to create a new promotion And I specify its code as "christmas_sale" And I name it "Christmas sale" - And I add the "Item percentage discount" action configured without a percentage value + And I add the "Item percentage discount" action configured without a percentage value for "United States" channel And I try to add it Then I should be notified that this value should not be blank And promotion with name "Christmas sale" should not be added - @ui @javascript + @api @ui @javascript Scenario: Trying to add a new promotion with a wrong order percentage discount When I want to create a new promotion And I specify its code as "christmas_sale" And I name it "Christmas sale" - And I add the "Order percentage discount" action configured with a percentage value of 120% + And I add the "Order percentage discount" action configured with a percentage value of "120%" And I try to add it Then I should be notified that a percentage discount value must be between 0% and 100% And promotion with name "Christmas sale" should not be added - @ui @javascript + @api @ui @javascript Scenario: Trying to add a new promotion with a wrong item percentage discount When I want to create a new promotion And I specify its code as "christmas_sale" And I name it "Christmas sale" - And I add the "Item percentage discount" action configured with a percentage value of 130% + And I add the "Item percentage discount" action configured with a percentage value of "130%" for "United States" channel And I try to add it Then I should be notified that a percentage discount value must be between 0% and 100% And promotion with name "Christmas sale" should not be added diff --git a/features/promotion/managing_promotions/promotions_filter_validation.feature b/features/promotion/managing_promotions/promotions_filter_validation.feature index e2168a7209..6fe1bd3044 100644 --- a/features/promotion/managing_promotions/promotions_filter_validation.feature +++ b/features/promotion/managing_promotions/promotions_filter_validation.feature @@ -8,24 +8,24 @@ Feature: Promotion filters validation Given the store operates on a single channel in "United States" And I am logged in as an administrator - @ui @javascript + @api @ui @javascript Scenario: Adding a promotion with wrong minimum price on price range filter When I want to create a new promotion And I specify its code as "10_for_all_products_over_10" And I name it "$10 discount for all products over $10!" - And I add the "Item percentage discount" action configured with a percentage value of 10% for "United States" channel - And I specify that on "United States" channel this action should be applied to items with price greater then "$asdasd" + And I add the "Item percentage discount" action configured with a percentage value of "10%" for "United States" channel + And I specify that on "United States" channel this action should be applied to items with price greater than "$asdasd" And I try to add it Then I should be notified that a minimum value should be a numeric value And promotion with name "$10 discount for all products over $10!" should not be added - @ui @javascript + @api @ui @javascript Scenario: Adding a promotion with wrong maximum price on price range filter When I want to create a new promotion And I specify its code as "10_for_all_products_over_10" And I name it "$10 discount for (almost) all products!" - And I add the "Item percentage discount" action configured with a percentage value of 10% for "United States" channel - And I specify that on "United States" channel this action should be applied to items with price lesser then "$asdasda" + And I add the "Item percentage discount" action configured with a percentage value of "10%" for "United States" channel + And I specify that on "United States" channel this action should be applied to items with price lesser than "$asdasda" And I try to add it Then I should be notified that a maximum value should be a numeric value And promotion with name "$10 discount for (almost) all products!" should not be added diff --git a/features/promotion/managing_promotions/seeing_correct_percantage_discounts_while_editing_promotion_with_action_and_decimal_places.feature b/features/promotion/managing_promotions/seeing_correct_percantage_discounts_while_editing_promotion_with_action_and_decimal_places.feature index f02e3d1f05..33a6bb9fdf 100644 --- a/features/promotion/managing_promotions/seeing_correct_percantage_discounts_while_editing_promotion_with_action_and_decimal_places.feature +++ b/features/promotion/managing_promotions/seeing_correct_percantage_discounts_while_editing_promotion_with_action_and_decimal_places.feature @@ -10,7 +10,7 @@ Feature: Seeing correct percentage discounts while editing promotion with action And this promotion gives "12.00%" discount to every order And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing an accurate percentage amount after editing the promotion including the value up to one decimal place When I want to modify a "Cheap Stuff" promotion And I edit this promotion percentage action to have "2.5%" @@ -18,7 +18,7 @@ Feature: Seeing correct percentage discounts while editing promotion with action Then I should be notified that it has been successfully edited And it should have "2.50%" of order percentage discount - @ui + @api @ui Scenario: Seeing an accurate percentage amount after editing the promotion including the value up to two decimal places When I want to modify a "Cheap Stuff" promotion And I edit this promotion percentage action to have "2.56%" @@ -26,7 +26,7 @@ Feature: Seeing correct percentage discounts while editing promotion with action Then I should be notified that it has been successfully edited And it should have "2.56%" of order percentage discount - @ui + @ui @no-api Scenario: Seeing an accurate percentage amount after using a comma as a decimal separator When I want to modify a "Cheap Stuff" promotion And I edit this promotion percentage action to have "2,56%" diff --git a/features/promotion/managing_promotions/sorting_promotions_by_priority.feature b/features/promotion/managing_promotions/sorting_promotions_by_priority.feature index 2b8bf5b1bc..cd44a2e403 100644 --- a/features/promotion/managing_promotions/sorting_promotions_by_priority.feature +++ b/features/promotion/managing_promotions/sorting_promotions_by_priority.feature @@ -11,21 +11,21 @@ Feature: Sorting listed promotions by priority And there is a promotion "Pugs For Everyone" with priority 0 And I am logged in as an administrator - @ui + @api @ui Scenario: Promotions are sorted by priority in descending order by default When I want to browse promotions Then I should see 3 promotions on the list And the first promotion on the list should have name "Honour Harambe" And the last promotion on the list should have name "Pugs For Everyone" - @ui + @api @ui Scenario: Promotion's default priority is 0 which puts it at the bottom of the list Given there is a promotion "Flying Pigs" When I want to browse promotions Then I should see 4 promotions on the list And the last promotion on the list should have name "Flying Pigs" - @ui + @api @ui Scenario: Promotion added with priority -1 is set at the top of the list Given there is a promotion "Flying Pigs" with priority -1 When I want to browse promotions diff --git a/features/promotion/receiving_discount/not_applying_archived_promotions.feature b/features/promotion/receiving_discount/not_applying_archived_promotions.feature new file mode 100644 index 0000000000..139319b269 --- /dev/null +++ b/features/promotion/receiving_discount/not_applying_archived_promotions.feature @@ -0,0 +1,19 @@ +@receiving_discount +Feature: Not applying archived promotions to the cart + In order to not apply promotions that are archived + As a Visitor + I want to not receive discounts from archived promotions + + Background: + Given the store operates on a single channel in "United States" + And the store has a product "PHP T-Shirt" priced at "$100.00" + And there is a promotion "Christmas sale" + And this promotion gives "$10.00" discount to every order + + @api @ui + Scenario: Archived promotion is not applied to the cart + Given the promotion "Christmas sale" is archived + When I add product "PHP T-Shirt" to the cart + Then I should be on my cart summary page + And I should be notified that the product has been successfully added + And my cart total should be "$100.00" diff --git a/features/promotion/receiving_discount/receiving_discounts_with_product_minimum_price_specified.feature b/features/promotion/receiving_discount/receiving_discounts_with_product_minimum_price_specified.feature index 3a660e315f..398662ab8f 100644 --- a/features/promotion/receiving_discount/receiving_discounts_with_product_minimum_price_specified.feature +++ b/features/promotion/receiving_discount/receiving_discounts_with_product_minimum_price_specified.feature @@ -84,7 +84,7 @@ Feature: Receiving discounts with product minimum price specified @api Scenario: Distributing discount evenly between different products when one has minimum price specified - Given it gives "$25" discount to every order + Given it gives "$25.00" discount to every order And I add product "T-Shirt" to the cart And I add 3 products "PHP Mug" to the cart And I specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" @@ -95,7 +95,7 @@ Feature: Receiving discounts with product minimum price specified @api Scenario: Distributing discount evenly between different products when one has minimum price specified - Given it gives "$20" discount to every order + Given it gives "$20.00" discount to every order And I add 2 products "T-Shirt" to the cart And I add 3 products "PHP Mug" to the cart And I specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" @@ -106,7 +106,7 @@ Feature: Receiving discounts with product minimum price specified @api Scenario: Distributing discount proportionally between different products when one has minimum price specified - Given it gives "$27" discount to every order + Given it gives "$27.00" discount to every order And I add 2 products "T-Shirt" to the cart And I add 2 products "PHP Mug" to the cart And I add product "Symfony Mug" to the cart @@ -115,11 +115,11 @@ Feature: Receiving discounts with product minimum price specified Then I should be on the checkout summary step And the "T-Shirt" product should have unit prices discounted by "$5.00" and "$5.00" And the "PHP Mug" product should have unit prices discounted by "$4.25" and "$4.25" - And the "Symfony Mug" product should have unit price discounted by "$8.5" + And the "Symfony Mug" product should have unit price discounted by "$8.50" @api Scenario: Distributing discount proportionally between different products when one has minimum price specified - Given it gives "$36" discount to every order + Given it gives "$36.00" discount to every order And the store has a "Keyboard" configurable product And this product has "RGB Keyboard" variant priced at "$20.00" And the "RGB Keyboard" variant has minimum price of "$15.00" in the "United States" channel @@ -139,7 +139,7 @@ Feature: Receiving discounts with product minimum price specified @api Scenario: Distributing discount proportionally between different products when one has minimum price specified - Given it gives "$12" discount to every order + Given it gives "$12.00" discount to every order And the store has a product "Cup" priced at "$10.00" And the store has a "Keyboard" configurable product And this product has "RGB Keyboard" variant priced at "$20.00" @@ -164,7 +164,7 @@ Feature: Receiving discounts with product minimum price specified @api Scenario: Distributing discount proportionally between different products when one has minimum price specified - Given it gives "$12" discount to every order + Given it gives "$12.00" discount to every order And the store has a product "Cup" priced at "$10.00" And the store has a "Keyboard" configurable product And this product has "RGB Keyboard" variant priced at "$20.00" @@ -189,7 +189,7 @@ Feature: Receiving discounts with product minimum price specified @api Scenario: Distributing more than allowed discount proportionally between different products when one has minimum price specified - Given it gives "$20" discount to every order + Given it gives "$20.00" discount to every order And the store has a product "Cup" priced at "$10.00" And the store has a "Keyboard" configurable product And this product has "RGB Keyboard" variant priced at "$20.00" @@ -215,7 +215,7 @@ Feature: Receiving discounts with product minimum price specified @api Scenario: Distributing discount proportionally between different products when one has minimum price specified and promotion does not apply on discounted products Given this promotion does not apply on discounted products - And it gives "$27" discount to every order + And it gives "$27.00" discount to every order And there is a catalog promotion "Fixed T-Shirt sale" that reduces price by fixed "$2.50" in the "United States" channel and applies on "PHP Mug" product And I add 2 products "T-Shirt" to the cart And I add 2 products "PHP Mug" to the cart diff --git a/features/promotion/receiving_discount/receiving_percentage_discount_promotion_on_order.feature b/features/promotion/receiving_discount/receiving_percentage_discount_promotion_on_order.feature index bf34748c15..2d97cab545 100644 --- a/features/promotion/receiving_discount/receiving_percentage_discount_promotion_on_order.feature +++ b/features/promotion/receiving_discount/receiving_percentage_discount_promotion_on_order.feature @@ -28,7 +28,7 @@ Feature: Receiving percentage discount promotion on order @ui @api Scenario: Receiving percentage discount is correct for two items with different price - Given the store has a product "Vintage Watch" priced at "$1000.00" + Given the store has a product "Vintage Watch" priced at "$1,000.00" When I add product "PHP T-Shirt" to the cart And I add product "Vintage Watch" to the cart Then my cart total should be "$880.00" diff --git a/features/promotion/receiving_discount/receiving_stacked_promotions_with_changing_context.feature b/features/promotion/receiving_discount/receiving_stacked_promotions_with_changing_context.feature index 68ea9d7791..ed4f91ed03 100644 --- a/features/promotion/receiving_discount/receiving_stacked_promotions_with_changing_context.feature +++ b/features/promotion/receiving_discount/receiving_stacked_promotions_with_changing_context.feature @@ -11,7 +11,7 @@ Feature: Receiving stacked promotion with changing context And there is a promotion "Holiday promotion" with priority 1 And it gives "50%" discount to every order And there is a promotion "Free shiping over" with priority 0 - And it gives free shipping to every order over "$100" + And it gives free shipping to every order over "$100.00" @ui @api Scenario: Receiving only the "Holiday promotion" diff --git a/features/promotion/reverting_discount/reverting_previously_applied_discount_on_cart.feature b/features/promotion/reverting_discount/reverting_previously_applied_discount_on_cart.feature index 0546b20944..d17a1b8a8a 100644 --- a/features/promotion/reverting_discount/reverting_previously_applied_discount_on_cart.feature +++ b/features/promotion/reverting_discount/reverting_previously_applied_discount_on_cart.feature @@ -19,10 +19,10 @@ Feature: Reverting previously applied discount on cart Then my cart total should be "$20.00" And there should be no discount - @ui + @ui @api Scenario: Reverting discount applied from total item cost based promotion Given this promotion gives "10%" off on every product when the item total is at least "$100.00" And I have 8 products "PHP Mug" in the cart - When I change "PHP Mug" quantity to 4 + When I change product "PHP Mug" quantity to 4 Then product "PHP Mug" price should not be decreased And my cart total should be "$80.00" diff --git a/features/promotion/tracking_usage/checking_promotion_usage.feature b/features/promotion/tracking_usage/checking_promotion_usage.feature index d8fd2c9970..3cfae047d5 100644 --- a/features/promotion/tracking_usage/checking_promotion_usage.feature +++ b/features/promotion/tracking_usage/checking_promotion_usage.feature @@ -16,7 +16,7 @@ Feature: Checking a promotion usage after placing an order And there is a promotion "Christmas promotion" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing item fixed discount promotion usage unchanged after order placement Given the promotion gives "$10.00" off on every product with minimum price at "$50.00" And the promotion gives another "$5.00" off on every product classified as "T-Shirts" @@ -26,7 +26,7 @@ Feature: Checking a promotion usage after placing an order When I browse promotions Then the promotion "Christmas promotion" should not be used - @ui + @api @ui Scenario: Seeing item fixed discount promotion usage increased after order placement Given the promotion gives "$10.00" off on every product with minimum price at "$50.00" And the promotion gives another "$5.00" off on every product classified as "Mugs" @@ -36,7 +36,7 @@ Feature: Checking a promotion usage after placing an order When I browse promotions Then the promotion "Christmas promotion" should be used 1 time - @ui + @api @ui Scenario: Seeing shipping percentage discount promotion usage unchanged after order placement Given the promotion gives "100%" discount on shipping to every order And there is a customer "john.doe@gmail.com" that placed an order "#00000022" diff --git a/features/promotion/tracking_usage/decreasing_promotion_coupon_usage_after_cancelling_order.feature b/features/promotion/tracking_usage/decreasing_promotion_coupon_usage_after_cancelling_order.feature index 5a8204d9c4..e20c381ff9 100644 --- a/features/promotion/tracking_usage/decreasing_promotion_coupon_usage_after_cancelling_order.feature +++ b/features/promotion/tracking_usage/decreasing_promotion_coupon_usage_after_cancelling_order.feature @@ -12,7 +12,7 @@ Feature: Decreasing a promotion coupon usage after cancelling an order And the store has promotion "Christmas sale" with coupon "SANTA2016" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing promotion coupon usage decreased after order cancellation Given there is a customer "john.doe@gmail.com" that placed an order "#00000022" And the customer bought a single "PHP T-Shirt" using "SANTA2016" coupon @@ -21,7 +21,7 @@ Feature: Decreasing a promotion coupon usage after cancelling an order When I browse all coupons of "Christmas sale" promotion Then "SANTA2016" coupon should be used 0 times - @ui + @api @ui Scenario: Seeing promotion coupon usage decreased to 1 after second order cancellation Given there is a customer "john.doe@gmail.com" that placed an order "#00000022" And the customer bought a single "PHP T-Shirt" using "SANTA2016" coupon diff --git a/features/promotion/tracking_usage/decreasing_promotion_usage_after_cancelling_order.feature b/features/promotion/tracking_usage/decreasing_promotion_usage_after_cancelling_order.feature index 70f70f7f53..5b739c684c 100644 --- a/features/promotion/tracking_usage/decreasing_promotion_usage_after_cancelling_order.feature +++ b/features/promotion/tracking_usage/decreasing_promotion_usage_after_cancelling_order.feature @@ -12,7 +12,7 @@ Feature: Decreasing a promotion usage after cancelling an order And there is a promotion "Limited promotion" limited to 5 usages And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing promotion usage decreased after order cancellation Given there is a customer "john.doe@gmail.com" that placed an order "#00000022" And the customer bought a single "PHP T-Shirt" diff --git a/features/promotion/tracking_usage/increasing_promotion_coupon_usage_after_placing_order.feature b/features/promotion/tracking_usage/increasing_promotion_coupon_usage_after_placing_order.feature index 06cc8c901b..bea00a7c3f 100644 --- a/features/promotion/tracking_usage/increasing_promotion_coupon_usage_after_placing_order.feature +++ b/features/promotion/tracking_usage/increasing_promotion_coupon_usage_after_placing_order.feature @@ -12,7 +12,7 @@ Feature: Increasing a promotion coupon usage after placing an order And the store has promotion "Christmas sale" with coupon "SANTA2016" And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing promotion coupon usage increased after order placement Given there is a customer "john.doe@gmail.com" that placed an order "#00000022" And the customer bought a single "PHP T-Shirt" using "SANTA2016" coupon @@ -20,7 +20,7 @@ Feature: Increasing a promotion coupon usage after placing an order When I browse all coupons of "Christmas sale" promotion Then "SANTA2016" coupon should be used 1 time - @ui + @api @ui Scenario: Seeing promotion coupon usage increased correctly after few orders placement Given there is a customer "john.doe@gmail.com" that placed an order "#00000022" And the customer bought a single "PHP T-Shirt" using "SANTA2016" coupon diff --git a/features/promotion/tracking_usage/increasing_promotion_usage_after_placing_order.feature b/features/promotion/tracking_usage/increasing_promotion_usage_after_placing_order.feature index be78161f54..412a1c5e74 100644 --- a/features/promotion/tracking_usage/increasing_promotion_usage_after_placing_order.feature +++ b/features/promotion/tracking_usage/increasing_promotion_usage_after_placing_order.feature @@ -13,7 +13,7 @@ Feature: Increasing a promotion usage after placing an order And it gives "$10.00" discount to every order And I am logged in as an administrator - @ui + @api @ui Scenario: Seeing promotion usage increased after order placement Given there is a customer "john.doe@gmail.com" that placed an order "#00000022" And the customer bought a single "PHP T-Shirt" diff --git a/features/shipping/applying_shipping_method_rules/seeing_estimated_shipping_cost_based_on_items_total.feature b/features/shipping/applying_shipping_method_rules/seeing_estimated_shipping_cost_based_on_items_total.feature index 4b127868e6..8f92eb5fad 100644 --- a/features/shipping/applying_shipping_method_rules/seeing_estimated_shipping_cost_based_on_items_total.feature +++ b/features/shipping/applying_shipping_method_rules/seeing_estimated_shipping_cost_based_on_items_total.feature @@ -8,9 +8,9 @@ Feature: Seeing estimated shipping costs based on items total Given the store operates on a single channel in "United States" And the store has a product "Cheap Jacket" priced at "$20.00" And the store has a product "Expensive Jacket" priced at "$30.00" - And the store has "Above $30" shipping method with "$1" fee - And this shipping method is only available for orders over or equal to "$30" - And the store has "Below $29.99" shipping method with "$10" fee + And the store has "Above $30" shipping method with "$1.00" fee + And this shipping method is only available for orders over or equal to "$30.00" + And the store has "Below $29.99" shipping method with "$10.00" fee And this shipping method is only available for orders under or equal to "$29.99" And I am a logged in customer diff --git a/features/shipping/applying_shipping_method_rules/seeing_estimated_shipping_cost_based_on_total_weight.feature b/features/shipping/applying_shipping_method_rules/seeing_estimated_shipping_cost_based_on_total_weight.feature index 7bd8c79dcb..28bec1bb8b 100644 --- a/features/shipping/applying_shipping_method_rules/seeing_estimated_shipping_cost_based_on_total_weight.feature +++ b/features/shipping/applying_shipping_method_rules/seeing_estimated_shipping_cost_based_on_total_weight.feature @@ -6,13 +6,13 @@ Feature: Seeing estimated shipping costs based on total weight Background: Given the store operates on a single channel in "United States" - And the store has a product "Jacket for the Lochness Monster" priced at "$1337.00" + And the store has a product "Jacket for the Lochness Monster" priced at "$1,337.00" And this product's weight is 200 And the store has a product "T-Shirt for Tinkerbell" priced at "$1.00" And this product's weight is 0.1 - And the store has "Heavy Duty Courier" shipping method with "$200" fee + And the store has "Heavy Duty Courier" shipping method with "$200.00" fee And this shipping method is only available for orders with a total weight greater or equal to 100.0 - And the store has "Fairytale Delivery Service" shipping method with "$2" fee + And the store has "Fairytale Delivery Service" shipping method with "$2.00" fee And this shipping method is only available for orders with a total weight less or equal to 1.0 And I am a logged in customer diff --git a/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_items_total.feature b/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_items_total.feature index 35620ff723..2554650f97 100644 --- a/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_items_total.feature +++ b/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_items_total.feature @@ -8,11 +8,11 @@ Feature: Viewing available shipping methods based on items total Given the store operates on a single channel in "United States" And the store has a product "Cheap Jacket" priced at "$20.00" And the store has a product "Expensive Jacket" priced at "$50.00" - And the store has "Above $50" shipping method with "$1" fee - And this shipping method is only available for orders over or equal to "$50" - And the store has "Below $29.99" shipping method with "$10" fee + And the store has "Above $50" shipping method with "$1.00" fee + And this shipping method is only available for orders over or equal to "$50.00" + And the store has "Below $29.99" shipping method with "$10.00" fee And this shipping method is only available for orders under or equal to "$29.99" - And the store has "DHL" shipping method with "$20" fee + And the store has "DHL" shipping method with "$20.00" fee And I am a logged in customer @ui @api diff --git a/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_items_total_with_taxes_and_promotions.feature b/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_items_total_with_taxes_and_promotions.feature index b38805ff4b..88e0fe0b79 100644 --- a/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_items_total_with_taxes_and_promotions.feature +++ b/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_items_total_with_taxes_and_promotions.feature @@ -12,13 +12,13 @@ Feature: Viewing available shipping methods based on items total And it belongs to "Clothes" tax category And the store has a product "Expensive Jacket" priced at "$47.00" And it belongs to "Clothes" tax category - And the store has "Above $50" shipping method with "$1" fee within the "US" zone + And the store has "Above $50" shipping method with "$1.00" fee within the "US" zone And shipping method "Above $50" belongs to "Shipping Services" tax category - And this shipping method is only available for orders over or equal to "$50" - And the store has "Below $29.99" shipping method with "$20" fee within the "US" zone + And this shipping method is only available for orders over or equal to "$50.00" + And the store has "Below $29.99" shipping method with "$20.00" fee within the "US" zone And shipping method "Below $29.99" belongs to "Shipping Services" tax category And this shipping method is only available for orders under or equal to "$29.99" - And the store has "DHL" shipping method with "$20" fee + And the store has "DHL" shipping method with "$20.00" fee And there is a promotion "50% shipping discount" And it gives "50%" discount on shipping to every order And there is a promotion "Expensive promotion" diff --git a/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_total_weight.feature b/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_total_weight.feature index e268182be1..b57c985348 100644 --- a/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_total_weight.feature +++ b/features/shipping/applying_shipping_method_rules/viewing_available_shipping_methods_based_on_total_weight.feature @@ -6,14 +6,14 @@ Feature: Viewing available shipping methods based on total weight Background: Given the store operates on a single channel in "United States" - And the store has a product "Jacket for the Lochness Monster" priced at "$1337.00" + And the store has a product "Jacket for the Lochness Monster" priced at "$1,337.00" And this product's weight is 200 And the store has a product "T-Shirt for Tinkerbell" priced at "$1.00" And this product's weight is 0.1 - And the store has "DHL" shipping method with "$20" fee - And the store has "Heavy Duty Courier" shipping method with "$150" fee + And the store has "DHL" shipping method with "$20.00" fee + And the store has "Heavy Duty Courier" shipping method with "$150.00" fee And this shipping method is only available for orders with a total weight greater or equal to 100.0 - And the store has "Fairytale Delivery Service" shipping method with "$2" fee + And the store has "Fairytale Delivery Service" shipping method with "$2.00" fee And this shipping method is only available for orders with a total weight less or equal to 1.0 And I am a logged in customer diff --git a/features/shipping/managing_shipments/accessing_shipments_order_from_shipment_index.feature b/features/shipping/managing_shipments/accessing_shipments_order_from_shipment_index.feature index 62d88582aa..1e3067a39a 100644 --- a/features/shipping/managing_shipments/accessing_shipments_order_from_shipment_index.feature +++ b/features/shipping/managing_shipments/accessing_shipments_order_from_shipment_index.feature @@ -15,8 +15,8 @@ Feature: Accessing shipment's order from the shipments index And the customer chose "UPS" shipping method with "Cash on Delivery" payment And I am logged in as an administrator - @ui - Scenario: Accessing shipment's order from the shipments index + @ui @api + Scenario: Accessing shipment's order from the shipment When I browse shipments And I move to the details of first shipment's order - Then I should see order page with details of order "00000001" + Then I should see the details of order "#00000001" diff --git a/features/shipping/managing_shipping_categories/deleting_multiple_shipping_categories.feature b/features/shipping/managing_shipping_categories/deleting_multiple_shipping_categories.feature index 274e194f3b..e4e8dddeec 100644 --- a/features/shipping/managing_shipping_categories/deleting_multiple_shipping_categories.feature +++ b/features/shipping/managing_shipping_categories/deleting_multiple_shipping_categories.feature @@ -9,7 +9,7 @@ Feature: Deleting multiple shipping categories And the store has "Big" and "Small" shipping category And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple shipping categories at once When I browse shipping categories And I check the "Big" shipping category diff --git a/features/shipping/managing_shipping_methods/adding_shipping_method_with_rule.feature b/features/shipping/managing_shipping_methods/adding_shipping_method_with_rule.feature index b322197759..3e6206533b 100644 --- a/features/shipping/managing_shipping_methods/adding_shipping_method_with_rule.feature +++ b/features/shipping/managing_shipping_methods/adding_shipping_method_with_rule.feature @@ -10,7 +10,7 @@ Feature: Adding a new shipping method with rule And the store has a zone "United States" with code "US" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding a new shipping method with total weight greater than or equal rule When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -24,7 +24,7 @@ Feature: Adding a new shipping method with rule Then I should be notified that it has been successfully created And the shipping method "FedEx Carrier" should appear in the registry - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding a new shipping method with total weight less than or equal rule When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -38,7 +38,7 @@ Feature: Adding a new shipping method with rule Then I should be notified that it has been successfully created And the shipping method "FedEx Carrier" should appear in the registry - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding a new shipping method with order total greater than or equal rule When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -52,7 +52,7 @@ Feature: Adding a new shipping method with rule Then I should be notified that it has been successfully created And the shipping method "FedEx Carrier" should appear in the registry - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding a new shipping method with order total less than or equal rule When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" diff --git a/features/shipping/managing_shipping_methods/deleting_multiple_shipping_methods.feature b/features/shipping/managing_shipping_methods/deleting_multiple_shipping_methods.feature index 5619a927fc..2aa41a15dc 100644 --- a/features/shipping/managing_shipping_methods/deleting_multiple_shipping_methods.feature +++ b/features/shipping/managing_shipping_methods/deleting_multiple_shipping_methods.feature @@ -9,7 +9,7 @@ Feature: Deleting multiple shipping methods And the store allows shipping with "UPS", "FedEx" and "DHL" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple shipping methods at once When I browse shipping methods And I check the "UPS" shipping method diff --git a/features/shipping/managing_shipping_methods/editing_shipping_method.feature b/features/shipping/managing_shipping_methods/editing_shipping_method.feature index 63b9c64f7c..df12ec9df8 100644 --- a/features/shipping/managing_shipping_methods/editing_shipping_method.feature +++ b/features/shipping/managing_shipping_methods/editing_shipping_method.feature @@ -10,14 +10,6 @@ Feature: Editing shipping method And the store allows shipping with "UPS Carrier" identified by "UPS_CARRIER" And I am logged in as an administrator - @todo - Scenario: Trying to change shipping method code - When I want to modify a shipping method "UPS Carrier" - And I change its code to "UPS" - And I save my changes - Then I should be notified that code cannot be changed - And shipping method "UPS Carrier" should still have code "UPS_CARRIER" - @ui @api Scenario: Seeing disabled code field when editing shipping method When I want to modify a shipping method "UPS Carrier" diff --git a/features/shipping/managing_shipping_methods/seeing_errors_in_shipping_method_charges.feature b/features/shipping/managing_shipping_methods/seeing_errors_in_shipping_method_charges.feature index a0c675d767..ea4aef4c1f 100644 --- a/features/shipping/managing_shipping_methods/seeing_errors_in_shipping_method_charges.feature +++ b/features/shipping/managing_shipping_methods/seeing_errors_in_shipping_method_charges.feature @@ -13,7 +13,7 @@ Feature: Seeing errors in shipping method charges @ui @no-api Scenario: Seeing the number of errors with per shipment charges - Given the store has "FedEx Carrier" shipping method with "$20" fee per shipment for "Web" channel and "$15" for "Mobile" channel + Given the store has "FedEx Carrier" shipping method with "$20.00" fee per shipment for "Web" channel and "$15.00" for "Mobile" channel When I want to modify this shipping method And I remove the shipping charges of "Mobile" channel And I try to save my changes @@ -21,7 +21,7 @@ Feature: Seeing errors in shipping method charges @ui @no-api Scenario: Seeing the number of errors with per unit charges - Given the store has "FedEx Carrier" shipping method with "$20" fee per unit for "Web" channel and "$15" for "Mobile" channel + Given the store has "FedEx Carrier" shipping method with "$20.00" fee per unit for "Web" channel and "$15.00" for "Mobile" channel When I want to modify this shipping method And I remove the shipping charges of "Mobile" channel And I try to save my changes diff --git a/features/shipping/managing_shipping_methods/shipping_method_code_validation.feature b/features/shipping/managing_shipping_methods/shipping_method_code_validation.feature index 543a3c8cf6..f902f01a24 100644 --- a/features/shipping/managing_shipping_methods/shipping_method_code_validation.feature +++ b/features/shipping/managing_shipping_methods/shipping_method_code_validation.feature @@ -10,7 +10,7 @@ Feature: Shipping method code validation And the store has a zone "United States" with code "US" And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add a new shipping method with special symbols in the code When I want to create a new shipping method And I name it "FedEx Carrier" in "English (United States)" @@ -19,7 +19,7 @@ Feature: Shipping method code validation Then I should be notified that code needs to contain only specific symbols And shipping method with name "FedEx Carrier" should not be added - @ui + @ui @api Scenario: Trying to add a new shipping method with spaces in the code When I want to create a new shipping method And I name it "FedEx Carrier" in "English (United States)" @@ -27,14 +27,3 @@ Feature: Shipping method code validation And I try to add it Then I should be notified that code needs to contain only specific symbols And shipping method with name "FedEx Carrier" should not be added - - @ui @javascript - Scenario: Trying to add a new shipping method with - When I want to create a new shipping method - And I name it "FedEx Carrier First US Division" in "English (United States)" - And I specify its code as "PEC-US_01" - And I define it for the zone named "United States" - And I choose "Flat rate per shipment" calculator - And I specify its amount as 50 for "Web-US" channel - And I add it - Then the shipping method "FedEx Carrier First US Division" should appear in the registry diff --git a/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_shipment_validation.feature b/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_shipment_validation.feature index 9675d60b92..ac638f5396 100644 --- a/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_shipment_validation.feature +++ b/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_shipment_validation.feature @@ -10,7 +10,7 @@ Feature: Shipping method flat rate per shipment calculator validation And the store has a zone "United States" with code "US" And I am logged in as an administrator - @ui @javascript + @ui @javascript @api Scenario: Trying to add a new shipping method with flat rate per shipment calculator without specifying its amount When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -21,7 +21,7 @@ Feature: Shipping method flat rate per shipment calculator validation Then I should be notified that amount for "Web" channel should not be blank And shipping method with name "FedEx Carrier" should not be added - @ui @javascript + @ui @javascript @api Scenario: Trying to add a new shipping method with flat rate per shipment calculator with charge below 0 When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" diff --git a/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_unit_validation.feature b/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_unit_validation.feature index 1a5ff7c8fb..b5afb7ffa6 100644 --- a/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_unit_validation.feature +++ b/features/shipping/managing_shipping_methods/shipping_method_flat_rate_per_unit_validation.feature @@ -10,7 +10,7 @@ Feature: Shipping method flat rate per unit calculator validation And the store has a zone "United States" with code "US" And I am logged in as an administrator - @ui @javascript + @ui @javascript @api Scenario: Trying to add a new shipping method with flat rate per unit calculator without specifying its amount When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" @@ -21,7 +21,7 @@ Feature: Shipping method flat rate per unit calculator validation Then I should be notified that amount for "Web" channel should not be blank And shipping method with name "FedEx Carrier" should not be added - @ui @javascript + @ui @javascript @api Scenario: Trying to add a new shipping method with flat rate per unit calculator with charge below 0 When I want to create a new shipping method And I specify its code as "FED_EX_CARRIER" diff --git a/features/shipping/managing_shipping_methods/shipping_method_validation.feature b/features/shipping/managing_shipping_methods/shipping_method_validation.feature index af8f37c9c1..af85ae84a4 100644 --- a/features/shipping/managing_shipping_methods/shipping_method_validation.feature +++ b/features/shipping/managing_shipping_methods/shipping_method_validation.feature @@ -5,7 +5,8 @@ Feature: Shipping method validation I want to be prevented from adding it without specifying required fields Background: - Given the store operates on a single channel in "United States" + Given the store operates on a channel named "Web-US" in "USD" currency + And the store has a zone "United States" with code "US" And the store is available in "English (United States)" And I am logged in as an administrator @@ -46,10 +47,38 @@ Feature: Shipping method validation Then I should be notified that name is required And this shipping method should still be named "UPS Ground" - @ui + @ui @api Scenario: Trying to remove zone from existing shipping method Given the store allows shipping with "UPS Ground" When I want to modify this shipping method And I remove its zone And I try to save my changes - Then I should be notified that zone has to be selected + Then I should be notified that the zone is required + + @ui @mink:chromedriver @api + Scenario: Adding a new shipping method with order total greater than or equal rule that contains invalid data + When I want to create a new shipping method + And I specify its code as "FED_EX_CARRIER" + And I specify its position as 0 + And I name it "FedEx Carrier" in "English (United States)" + And I define it for the zone named "United States" + And I choose "Flat rate per shipment" calculator + And I specify its amount as 50 for "Web-US" channel + And I add the "Total weight greater than or equal" rule configured with invalid data + And I add it + Then I should be notified that the weight rule has an invalid configuration + And the shipping method "FedEx Carrier" should not appear in the registry + + @ui @mink:chromedriver @api + Scenario: Adding a new shipping method with order total less than or equal rule that contains invalid data + When I want to create a new shipping method + And I specify its code as "FED_EX_CARRIER" + And I specify its position as 0 + And I name it "FedEx Carrier" in "English (United States)" + And I define it for the zone named "United States" + And I choose "Flat rate per shipment" calculator + And I specify its amount as 50 for "Web-US" channel + And I add the "Items total less than or equal" rule configured with invalid data for "Web-US" channel + And I add it + Then I should be notified that the amount rule has an invalid configuration in "Web-US" channel + And the shipping method "FedEx Carrier" should not appear in the registry diff --git a/features/taxation/applying_taxes/applying_correct_taxes_for_shipping.feature b/features/taxation/applying_taxes/applying_correct_taxes_for_shipping.feature index 3433db8e4f..1f3d00e65c 100644 --- a/features/taxation/applying_taxes/applying_correct_taxes_for_shipping.feature +++ b/features/taxation/applying_taxes/applying_correct_taxes_for_shipping.feature @@ -17,7 +17,7 @@ Feature: Applying correct taxes for shipping And the store has "DHL" shipping method with "$10.00" fee within the "US" zone And shipping method "DHL" belongs to "Shipping Services" tax category - @ui + @ui @api Scenario: Applying correct taxes for shipping When I add product "PHP T-Shirt" to the cart Then my cart items total should be "$10.00" @@ -25,7 +25,7 @@ Feature: Applying correct taxes for shipping And my cart taxes should be "$1.50" And my cart total should be "$21.50" - @ui + @ui @api Scenario: Applying correct taxes for shipping When I add product "PHP Mug" to the cart Then my cart items total should be "$10.00" diff --git a/features/taxation/applying_taxes/applying_correct_taxes_for_shipping_with_tax_rate_included_in_price.feature b/features/taxation/applying_taxes/applying_correct_taxes_for_shipping_with_tax_rate_included_in_price.feature index 9c56353b74..67c9a0aaca 100644 --- a/features/taxation/applying_taxes/applying_correct_taxes_for_shipping_with_tax_rate_included_in_price.feature +++ b/features/taxation/applying_taxes/applying_correct_taxes_for_shipping_with_tax_rate_included_in_price.feature @@ -17,7 +17,7 @@ Feature: Applying correct taxes for shipping with tax rate included in price And the store has "DHL" shipping method with "$10.00" fee within the "US" zone And shipping method "DHL" belongs to "Shipping Services" tax category - @ui + @ui @api Scenario: Applying correct taxes for shipping with tax rate included in price When I add product "PHP T-Shirt" to the cart Then my cart items total should be "$10.00" @@ -26,7 +26,7 @@ Feature: Applying correct taxes for shipping with tax rate included in price And my cart included in price taxes should be "$0.91" And my cart total should be "$20.50" - @ui + @ui @api Scenario: Applying correct taxes for shipping with tax rate included in price When I add product "PHP Mug" to the cart Then my cart items total should be "$10.00" diff --git a/features/taxation/managing_tax_categories/deleting_multiple_tax_categories.feature b/features/taxation/managing_tax_categories/deleting_multiple_tax_categories.feature index c17f9b2e36..c1b6d5c71e 100644 --- a/features/taxation/managing_tax_categories/deleting_multiple_tax_categories.feature +++ b/features/taxation/managing_tax_categories/deleting_multiple_tax_categories.feature @@ -8,7 +8,7 @@ Feature: Deleting multiple tax categories Given the store has tax categories "Alcohol", "Food" and "Books" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple tax categories at once When I browse tax categories And I check the "Alcohol" tax category diff --git a/features/taxation/managing_tax_categories/editing_tax_category.feature b/features/taxation/managing_tax_categories/editing_tax_category.feature index 8d1172834d..fa33d446b2 100644 --- a/features/taxation/managing_tax_categories/editing_tax_category.feature +++ b/features/taxation/managing_tax_categories/editing_tax_category.feature @@ -8,16 +8,8 @@ Feature: Editing tax category Given the store has a tax category "Alcohol" with a code "alcohol" And I am logged in as an administrator - @todo - Scenario: Trying to change tax category code - When I want to modify a tax category "Alcohol" - And I change its code to "beverages" - And I save my changes - Then I should be notified that code cannot be changed - And tax category "Alcohol" should still have code "alcohol" - @ui @api - Scenario: Seeing disabled code field when editing tax category + Scenario: Inability of changing the code of an existing tax category When I want to modify a tax category "Alcohol" Then I should not be able to edit its code diff --git a/features/taxation/managing_tax_rates/adding_tax_rate.feature b/features/taxation/managing_tax_rates/adding_tax_rate.feature index cfe5f817be..85cc677755 100644 --- a/features/taxation/managing_tax_rates/adding_tax_rate.feature +++ b/features/taxation/managing_tax_rates/adding_tax_rate.feature @@ -9,7 +9,7 @@ Feature: Adding a new tax rate And the store has a tax category "Food and Beverage" And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new tax rate When I want to create a new tax rate And I specify its code as "US_SALES_TAX" @@ -22,7 +22,7 @@ Feature: Adding a new tax rate Then I should be notified that it has been successfully created And the tax rate "United States Sales Tax" should appear in the registry - @ui + @api @ui Scenario: Adding a zero tax rate When I want to create a new tax rate And I specify its code as "US_SALES_TAX" @@ -35,7 +35,7 @@ Feature: Adding a new tax rate Then I should be notified that it has been successfully created And the tax rate "United States Sales Tax" should appear in the registry - @ui + @api @ui Scenario: Adding a new tax rate with start and end date When I want to create a new tax rate And I specify its code as "US_SALES_TAX" @@ -49,7 +49,7 @@ Feature: Adding a new tax rate Then I should be notified that it has been successfully created And the tax rate "United States Sales Tax" should appear in the registry - @ui + @api @ui Scenario: Adding a new tax rate with start date only When I want to create a new tax rate And I specify its code as "US_SALES_TAX" @@ -63,7 +63,7 @@ Feature: Adding a new tax rate Then I should be notified that it has been successfully created And the tax rate "United States Sales Tax" should appear in the registry - @ui + @api @ui Scenario: Adding a new tax rate with end date only When I want to create a new tax rate And I specify its code as "US_SALES_TAX" @@ -77,7 +77,7 @@ Feature: Adding a new tax rate Then I should be notified that it has been successfully created And the tax rate "United States Sales Tax" should appear in the registry - @ui @javascript + @api @ui @mink:chromedriver Scenario: Adding a new tax rate which will be included in product price When I want to create a new tax rate And I specify its code as "US_SALES_TAX" @@ -90,3 +90,17 @@ Feature: Adding a new tax rate And I add it Then I should be notified that it has been successfully created And the tax rate "United States Sales Tax" should appear in the registry + And the tax rate "United States Sales Tax" should be included in price + + @api @no-ui + Scenario: Adding a new tax rate with no amount sets the default + When I want to create a new tax rate + And I specify its code as "US_SALES_TAX" + And I name it "United States Sales Tax" + And I define it for the "United States" zone + And I make it applicable for the "Food and Beverage" tax category + And I choose the default tax calculator + And I add it + Then I should be notified that it has been successfully created + And the tax rate "United States Sales Tax" should appear in the registry + And this tax rate amount should be 0% diff --git a/features/taxation/managing_tax_rates/deleting_multiple_tax_rates.feature b/features/taxation/managing_tax_rates/deleting_multiple_tax_rates.feature index 950d3a3457..8cc35ebfe1 100644 --- a/features/taxation/managing_tax_rates/deleting_multiple_tax_rates.feature +++ b/features/taxation/managing_tax_rates/deleting_multiple_tax_rates.feature @@ -11,7 +11,7 @@ Feature: Deleting multiple tax rates And the store has "High VAT" tax rate of 40% for "Food" for the rest of the world And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple tax rates at once When I browse tax rates And I check the "Low VAT" tax rate diff --git a/features/taxation/managing_tax_rates/deleting_tax_rate.feature b/features/taxation/managing_tax_rates/deleting_tax_rate.feature index 0fe92dced4..b69087448a 100644 --- a/features/taxation/managing_tax_rates/deleting_tax_rate.feature +++ b/features/taxation/managing_tax_rates/deleting_tax_rate.feature @@ -10,7 +10,7 @@ Feature: Deleting a tax rate And the store has "United States Sales Tax" tax rate of 20% for "Sports gear" within the "US" zone And I am logged in as an administrator - @ui + @ui @api Scenario: Deleted tax rate should disappear from the registry When I delete tax rate "United States Sales Tax" Then I should be notified that it has been successfully deleted diff --git a/features/taxation/managing_tax_rates/editing_tax_rate.feature b/features/taxation/managing_tax_rates/editing_tax_rate.feature index a8476feaef..73b53e2c79 100644 --- a/features/taxation/managing_tax_rates/editing_tax_rate.feature +++ b/features/taxation/managing_tax_rates/editing_tax_rate.feature @@ -10,20 +10,12 @@ Feature: Editing tax rate And the store has "United States Sales Tax" tax rate of 20% for "Sports gear" within the "US" zone And I am logged in as an administrator - @todo - Scenario: Trying to change tax rate code + @ui @api + Scenario: Inability of changing the code of an existing tax rate When I want to modify a tax rate "United States Sales Tax" - And I change its code to "us_vat" - And I save my changes - Then I should be notified that code cannot be changed - And tax rate "United States Sales Tax" should still have code "united_states_sales_tax" + Then I should not be able to edit its code - @ui - Scenario: Seeing disabled code field when editing tax rate - When I want to modify a tax rate "United States Sales Tax" - Then the code field should be disabled - - @ui + @ui @api Scenario: Renaming the tax rate When I want to modify a tax rate "United States Sales Tax" And I rename it to "US VAT" @@ -31,7 +23,7 @@ Feature: Editing tax rate Then I should be notified that it has been successfully edited And this tax rate name should be "US VAT" - @ui + @ui @api Scenario: Changing the tax rate amount When I want to modify a tax rate "United States Sales Tax" And I specify its amount as 16% @@ -39,7 +31,7 @@ Feature: Editing tax rate Then I should be notified that it has been successfully edited And this tax rate amount should be 16% - @ui + @ui @api Scenario: Changing related tax category Given the store has a tax category "Food and Beverage" also When I want to modify a tax rate "United States Sales Tax" @@ -48,7 +40,7 @@ Feature: Editing tax rate Then I should be notified that it has been successfully edited And this tax rate should be applicable for the "Food and Beverage" tax category - @ui + @ui @api Scenario: Changing related zone Given there is a zone "The Rest of the World" containing all other countries When I want to modify a tax rate "United States Sales Tax" diff --git a/features/taxation/managing_tax_rates/tax_rate_unique_code_validation.feature b/features/taxation/managing_tax_rates/tax_rate_unique_code_validation.feature index 36430b125b..091eb909e8 100644 --- a/features/taxation/managing_tax_rates/tax_rate_unique_code_validation.feature +++ b/features/taxation/managing_tax_rates/tax_rate_unique_code_validation.feature @@ -10,7 +10,7 @@ Feature: Tax rate unique code validation And the store has "United States Sales Tax" tax rate of 20% for "Sports gear" within the "US" zone identified by the "UNITED_STATES_SALES_TAX" code And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add tax rate with taken code When I want to create a new tax rate And I specify its code as "UNITED_STATES_SALES_TAX" diff --git a/features/taxation/managing_tax_rates/tax_rate_validation.feature b/features/taxation/managing_tax_rates/tax_rate_validation.feature index 11ee242f2a..250da8451e 100644 --- a/features/taxation/managing_tax_rates/tax_rate_validation.feature +++ b/features/taxation/managing_tax_rates/tax_rate_validation.feature @@ -9,7 +9,7 @@ Feature: Tax rate validation And the store has a tax category "Food and Beverage" And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add a new tax rate without specifying its code When I want to create a new tax rate And I name it "Food and Beverage Tax Rates" @@ -18,7 +18,7 @@ Feature: Tax rate validation Then I should be notified that code is required And tax rate with name "Food and Beverage Tax Rates" should not be added - @ui + @ui @no-api Scenario: Trying to add a new tax rate without specifying its amount When I want to create a new tax rate And I name it "Food and Beverage Tax Rates" @@ -27,7 +27,7 @@ Feature: Tax rate validation Then I should be notified that amount is required And tax rate with name "Food and Beverage Tax Rates" should not be added - @ui + @ui @api Scenario: Trying to add a new tax rate without specifying its name When I want to create a new tax rate And I specify its code as "UNITED_STATES_SALES_TAX" @@ -36,7 +36,7 @@ Feature: Tax rate validation Then I should be notified that name is required And tax rate with code "UNITED_STATES_SALES_TAX" should not be added - @ui + @ui @api Scenario: Trying to add a new tax rate without specifying its zone Given the store does not have any zones defined When I want to create a new tax rate @@ -46,7 +46,7 @@ Feature: Tax rate validation Then I should be notified that zone has to be selected And tax rate with name "Food and Beverage Tax Rates" should not be added - @ui + @ui @api Scenario: Trying to add a new tax rate without specifying its category Given the store does not have any categories defined When I want to create a new tax rate @@ -56,7 +56,7 @@ Feature: Tax rate validation Then I should be notified that category has to be selected And tax rate with name "Food and Beverage Tax Rates" should not be added - @ui + @ui @no-api Scenario: Trying to remove amount from existing tax rate Given the store has "United States Sales Tax" tax rate of 20% for "Sports gear" within the "US" zone When I want to modify this tax rate @@ -65,7 +65,7 @@ Feature: Tax rate validation Then I should be notified that amount is required And this tax rate amount should still be 20% - @ui + @ui @api Scenario: Trying to remove name from existing tax rate Given the store has "United States Sales Tax" tax rate of 20% for "Sports gear" within the "US" zone When I want to modify this tax rate @@ -74,7 +74,7 @@ Feature: Tax rate validation Then I should be notified that name is required And this tax rate should still be named "United States Sales Tax" - @ui + @ui @no-api Scenario: Trying to remove zone from existing tax rate Given the store has "United States Sales Tax" tax rate of 20% for "Sports gear" within the "US" zone When I want to modify this tax rate @@ -82,7 +82,7 @@ Feature: Tax rate validation And I try to save my changes Then I should be notified that zone has to be selected - @ui + @ui @api Scenario: Trying to add a new tax rate with negative amount When I want to create a new tax rate And I name it "Food and Beverage Tax Rates" @@ -91,7 +91,7 @@ Feature: Tax rate validation Then I should be notified that amount is invalid And tax rate with name "Food and Beverage Tax Rates" should not be added - @ui + @ui @api Scenario: Trying to add a new tax rate with end date before start date When I want to create a new tax rate And I name it "Food and Beverage Tax Rates" diff --git a/features/taxation/managing_tax_rates/tax_rates_filtration/filtering_tax_rates_by_end_date.feature b/features/taxation/managing_tax_rates/tax_rates_filtration/filtering_tax_rates_by_end_date.feature index 109749ee28..1049ad7589 100644 --- a/features/taxation/managing_tax_rates/tax_rates_filtration/filtering_tax_rates_by_end_date.feature +++ b/features/taxation/managing_tax_rates/tax_rates_filtration/filtering_tax_rates_by_end_date.feature @@ -4,7 +4,7 @@ Feature: Filtering tax rates by end date As an Administrator I want to be able to filter tax rates on the list - Background: + Background: Given the store operates on a single channel in "United States" And the store has "2022 tax rate" tax rate of 50% for "Clothes" within the "US" zone with dates between "2022-01-01" and "2022-12-31" And the store has "2023 tax rate" tax rate of 15% for "Clothes" within the "US" zone with dates between "2023-01-01" and "2023-12-31" @@ -20,7 +20,7 @@ Feature: Filtering tax rates by end date And I should see the tax rate "3 weeks tax rate" in the list @ui @api - Scenario: Filtering catalog promotions up to end date + Scenario: Filtering tax rates up to end date When I browse tax rates And I filter tax rates by end date up to "2022-12-31" Then I should not see a tax rate with name "2023 tax rate" @@ -28,7 +28,7 @@ Feature: Filtering tax rates by end date But I should see the tax rate "2022 tax rate" in the list @ui @api - Scenario: Filtering catalog promotions in a end date range + Scenario: Filtering tax rates in a end date range When I browse tax rates And I filter tax rates by end date from "2023-01-02" up to "2023-01-31" Then I should not see a tax rate with name "2023 tax rate" diff --git a/features/taxonomy/managing_taxons/adding_images_to_existing_taxon.feature b/features/taxonomy/managing_taxons/adding_images_to_existing_taxon.feature index 8086d4fb8d..6408428459 100644 --- a/features/taxonomy/managing_taxons/adding_images_to_existing_taxon.feature +++ b/features/taxonomy/managing_taxons/adding_images_to_existing_taxon.feature @@ -9,37 +9,37 @@ Feature: Adding images to an existing taxon And the store classifies its products as "T-Shirts" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding a single image to an existing taxon When I want to modify the "T-Shirts" taxon - And I attach the "t-shirts.jpg" image with "banner" type - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "t-shirts.jpg" image with "banner" type to this taxon + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And this taxon should have an image with "banner" type - @ui @javascript + @ui @mink:chromedriver @api Scenario: Adding a single image to an existing taxon without specifying the type When I want to modify the "T-Shirts" taxon - And I attach the "t-shirts.jpg" image - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "t-shirts.jpg" image to this taxon + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And this taxon should have only one image - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Adding multiple images to an existing taxon When I want to modify the "T-Shirts" taxon - And I attach the "t-shirts.jpg" image with "banner" type - And I attach the "t-shirts.jpg" image with "thumbnail" type - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "t-shirts.jpg" image with "banner" type to this taxon + And I attach the "t-shirts.jpg" image with "thumbnail" type to this taxon + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And this taxon should have an image with "banner" type And it should also have an image with "thumbnail" type - @ui @javascript + @ui @javascript @api Scenario: Adding multiple images of the same type to an existing taxon When I want to modify the "T-Shirts" taxon - And I attach the "t-shirts.jpg" image with "banner" type - And I attach the "t-shirts.jpg" image with "banner" type - And I save my changes - Then I should be notified that it has been successfully edited + And I attach the "t-shirts.jpg" image with "banner" type to this taxon + And I attach the "t-shirts.jpg" image with "banner" type to this taxon + And I save my changes to the images + Then I should be notified that it has been successfully uploaded And this taxon should have 2 images diff --git a/features/taxonomy/managing_taxons/adding_taxon.feature b/features/taxonomy/managing_taxons/adding_taxon.feature index c1d3f01015..8c13291ac8 100644 --- a/features/taxonomy/managing_taxons/adding_taxon.feature +++ b/features/taxonomy/managing_taxons/adding_taxon.feature @@ -8,7 +8,7 @@ Feature: Adding a new taxon Given the store is available in "English (United States)" And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new taxon When I want to create a new taxon And I specify its code as "t-shirts" @@ -18,7 +18,7 @@ Feature: Adding a new taxon Then I should be notified that it has been successfully created And the "T-Shirts" taxon should appear in the registry - @ui + @ui @api Scenario: Adding a new taxon with slug and description When I want to create a new taxon And I specify its code as "category" diff --git a/features/taxonomy/managing_taxons/adding_taxon_for_parent.feature b/features/taxonomy/managing_taxons/adding_taxon_for_parent.feature index 5e70fcc6c8..6f968b64f5 100644 --- a/features/taxonomy/managing_taxons/adding_taxon_for_parent.feature +++ b/features/taxonomy/managing_taxons/adding_taxon_for_parent.feature @@ -9,7 +9,7 @@ Feature: Adding a new taxon for parent And the store has "Category" taxonomy And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a new taxon for specific parent taxon When I want to create a new taxon for "Category" And I specify its code as "guns" diff --git a/features/taxonomy/managing_taxons/adding_taxon_with_images.feature b/features/taxonomy/managing_taxons/adding_taxon_with_images.feature index 0106bd65cf..231859d48d 100644 --- a/features/taxonomy/managing_taxons/adding_taxon_with_images.feature +++ b/features/taxonomy/managing_taxons/adding_taxon_with_images.feature @@ -8,7 +8,7 @@ Feature: Adding a new taxon with images Given the store is available in "English (United States)" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Adding a new taxon with a single image When I want to create a new taxon And I specify its code as "t-shirts" @@ -19,7 +19,7 @@ Feature: Adding a new taxon with images And the "T-Shirts" taxon should appear in the registry And this taxon should have an image with "banner" type - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Adding a new taxon with multiple images When I want to create a new taxon And I specify its code as "t-shirts" diff --git a/features/taxonomy/managing_taxons/adding_taxon_for_with_parent.feature b/features/taxonomy/managing_taxons/adding_taxon_with_parent.feature similarity index 97% rename from features/taxonomy/managing_taxons/adding_taxon_for_with_parent.feature rename to features/taxonomy/managing_taxons/adding_taxon_with_parent.feature index c0496399d7..6785abf8fc 100644 --- a/features/taxonomy/managing_taxons/adding_taxon_for_with_parent.feature +++ b/features/taxonomy/managing_taxons/adding_taxon_with_parent.feature @@ -9,7 +9,7 @@ Feature: Adding a new taxon with parent specified And the store has "Category" taxonomy And I am logged in as an administrator - @ui @javascript + @ui @javascript @api Scenario: Adding a new taxon with parent When I want to create a new taxon And I set its parent taxon to "Category" diff --git a/features/taxonomy/managing_taxons/autocomplete_taxon_name.feature b/features/taxonomy/managing_taxons/autocomplete_taxon_name.feature index d7570e4752..7a9f64e67d 100644 --- a/features/taxonomy/managing_taxons/autocomplete_taxon_name.feature +++ b/features/taxonomy/managing_taxons/autocomplete_taxon_name.feature @@ -1,4 +1,4 @@ -@managing_taxons +@managing_taxons_ajax Feature: Taxons autocomplete In order to get hints when looking for taxons As an Administrator diff --git a/features/taxonomy/managing_taxons/browsing_taxon.feature b/features/taxonomy/managing_taxons/browsing_taxon.feature index 46f461c7ac..203531ea23 100644 --- a/features/taxonomy/managing_taxons/browsing_taxon.feature +++ b/features/taxonomy/managing_taxons/browsing_taxon.feature @@ -8,7 +8,7 @@ Feature: Browsing taxons Given the store classifies its products as "T-Shirts" and "Accessories" And I am logged in as an administrator - @ui + @ui @api Scenario: Browsing taxons in store When I want to see all taxons in store Then I should see 2 taxons on the list diff --git a/features/taxonomy/managing_taxons/changing_images_of_taxon.feature b/features/taxonomy/managing_taxons/changing_images_of_taxon.feature index f9f15a4635..21820c9c3f 100644 --- a/features/taxonomy/managing_taxons/changing_images_of_taxon.feature +++ b/features/taxonomy/managing_taxons/changing_images_of_taxon.feature @@ -9,7 +9,7 @@ Feature: Changing images of an existing taxon And the store classifies its products as "T-Shirts" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Changing a single image of a taxon Given the "T-Shirts" taxon has an image "ford.jpg" with "banner" type When I want to modify the "T-Shirts" taxon @@ -18,13 +18,13 @@ Feature: Changing images of an existing taxon Then I should be notified that it has been successfully edited And this taxon should have an image with "banner" type - @ui @javascript + @ui @javascript @api Scenario: Changing the type of image of a taxon Given the "T-Shirts" taxon has an image "ford.jpg" with "thumbnail" type And the "T-Shirts" taxon also has an image "t-shirts.jpg" with "banner" type When I want to modify the "T-Shirts" taxon And I change the first image type to "banner" - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this taxon should still have 2 images But it should not have any images with "thumbnail" type diff --git a/features/taxonomy/managing_taxons/choosing_taxon_from_tree.feature b/features/taxonomy/managing_taxons/choosing_taxon_from_tree.feature index 1d5f45d297..14543ab3f3 100644 --- a/features/taxonomy/managing_taxons/choosing_taxon_from_tree.feature +++ b/features/taxonomy/managing_taxons/choosing_taxon_from_tree.feature @@ -1,4 +1,4 @@ -@managing_taxons +@managing_taxons_ajax Feature: Browsing taxons tree In order to see all taxons in the store As an Administrator diff --git a/features/taxonomy/managing_taxons/deleting_taxon.feature b/features/taxonomy/managing_taxons/deleting_taxon.feature index 99b89bdd96..749b08ab21 100644 --- a/features/taxonomy/managing_taxons/deleting_taxon.feature +++ b/features/taxonomy/managing_taxons/deleting_taxon.feature @@ -8,13 +8,13 @@ Feature: Deleting a taxon Given I am logged in as an administrator And the store operates on a channel named "Web Store" - @ui @javascript + @ui @javascript @api Scenario: Deleted taxon should disappear from the registry Given the store classifies its products as "T-Shirts" When I delete taxon named "T-Shirts" Then the taxon named "T-Shirts" should no longer exist in the registry - @ui @javascript + @ui @javascript @api Scenario: Deleting a taxon with a child does not delete any other taxons Given the store classifies its products as "Main catalog" And the "Main catalog" taxon has children taxon "Shoes" and "Shovels" @@ -25,14 +25,23 @@ Feature: Deleting a taxon And the taxon named "Women" should no longer exist in the registry But the "Shovels" taxon should appear in the registry - @ui @javascript + @ui @javascript @api Scenario: Being unable to delete a menu taxon of a channel Given the store classifies its products as "T-Shirts" and "Caps" And channel "Web Store" has menu taxon "Caps" When I try to delete taxon named "Caps" Then I should be notified that I cannot delete a menu taxon of any channel - @ui @javascript + @ui @javascript @api + Scenario: Deleting a taxon that is a main taxon of a product + Given the store classifies its products as "T-Shirts" + And the store has a product "T-Shirts PHP" + And this product has a main taxon "T-Shirts" + When I delete taxon named "T-Shirts" + Then the taxon named "T-Shirts" should no longer exist in the registry + And the product "T-Shirts PHP" should no longer have a main taxon + + @ui @javascript @api Scenario: Deleting root taxon above menu taxon Given the store has "Main Category" taxonomy And the store has "Clothes Category" taxonomy diff --git a/features/taxonomy/managing_taxons/displaying_products_in_proper_taxon_after_trying_to_delete_taxon.feature b/features/taxonomy/managing_taxons/displaying_products_in_proper_taxon_after_trying_to_delete_taxon.feature deleted file mode 100644 index 5bdf105d74..0000000000 --- a/features/taxonomy/managing_taxons/displaying_products_in_proper_taxon_after_trying_to_delete_taxon.feature +++ /dev/null @@ -1,29 +0,0 @@ -@managing_taxons -Feature: Displaying products in proper taxon after trying to delete this taxon - In order not to mess up the products to taxons assignments - As an Administrator - I want to keep the taxons structure the same after failed taxon deletion - - Background: - Given the store operates on a channel named "Web" - And the store classifies its products as "T-Shirts" - And the "T-Shirts" taxon has children taxon "Men" and "Women" - And the store has a product "T-Shirt Coconut" available in "Web" channel - And this product belongs to "T-Shirts" - And the store has a product "T-Shirt Banana" available in "Web" channel - And this product belongs to "Men" - And the product "T-Shirt Banana" has a main taxon "Men" - And the store has a product "T-Shirt Apple" available in "Web" channel - And this product belongs to "Men" - And the store has a product "T-Shirt Pear" available in "Web" channel - And this product belongs to "Women" - And the store has a product "T-Shirt Watermelon" available in "Web" channel - And this product belongs to "Women" - And I am logged in as an administrator - - @ui @javascript - Scenario: Displaying products in proper taxon after trying to delete this taxon - When I try to delete taxon named "Men" - Then I should be notified that I cannot delete a taxon in use - And I should see in taxon "Men" in the store products "T-Shirt Banana" and "T-Shirt Apple" - But I should not see in taxon "Men" in the store products "T-Shirt Pear" and "T-Shirt Watermelon" diff --git a/features/taxonomy/managing_taxons/editing_taxon.feature b/features/taxonomy/managing_taxons/editing_taxon.feature index 91125a2b6e..eeec5248a6 100644 --- a/features/taxonomy/managing_taxons/editing_taxon.feature +++ b/features/taxonomy/managing_taxons/editing_taxon.feature @@ -9,7 +9,7 @@ Feature: Editing a taxon And the store classifies its products as "T-Shirts" and "Accessories" And I am logged in as an administrator - @ui + @ui @api Scenario: Renaming a taxon When I want to modify the "T-Shirts" taxon And I rename it to "Stickers" in "English (United States)" @@ -17,27 +17,23 @@ Feature: Editing a taxon Then I should be notified that it has been successfully edited And this taxon name should be "Stickers" - @ui + @ui @api Scenario: Changing description When I want to modify the "T-Shirts" taxon - And I rename it to "Stickers" in "English (United States)" And I change its description to "Main taxonomy for stickers" in "English (United States)" And I save my changes Then I should be notified that it has been successfully edited And this taxon description should be "Main taxonomy for stickers" - @ui @javascript + @ui @javascript @api Scenario: Changing parent taxon When I want to modify the "T-Shirts" taxon - And I rename it to "Stickers" in "English (United States)" - And I change its description to "Main taxonomy for stickers" in "English (United States)" - And I set its slug to "stickers" in "English (United States)" And I change its parent taxon to "Accessories" And I save my changes Then I should be notified that it has been successfully edited And this taxon should belongs to "Accessories" - @ui - Scenario: Seeing a disabled code field when editing a taxon + @ui @api + Scenario: Being unable to change code of taxon When I want to modify the "T-Shirts" taxon - Then the code field should be disabled + Then I should not be able to edit its code diff --git a/features/taxonomy/managing_taxons/editing_taxon_slug.feature b/features/taxonomy/managing_taxons/editing_taxon_slug.feature index 6332d49881..950cb0789d 100644 --- a/features/taxonomy/managing_taxons/editing_taxon_slug.feature +++ b/features/taxonomy/managing_taxons/editing_taxon_slug.feature @@ -8,7 +8,7 @@ Feature: Editing taxon's slug Given the store is available in "English (United States)" And I am logged in as an administrator - @ui @javascript + @ui @javascript @api Scenario: Creating a root taxon with an autogenerated slug When I want to create a new taxon And I specify its code as "MEDIEVAL_WEAPONS" @@ -16,7 +16,7 @@ Feature: Editing taxon's slug And I add it Then this taxon slug should be "medieval-weapons" - @ui + @ui @api Scenario: Creating a root taxon with a custom slug When I want to create a new taxon And I specify its code as "MEDIEVAL_WEAPONS" @@ -25,8 +25,8 @@ Feature: Editing taxon's slug And I add it Then this taxon slug should be "mw" - @ui @javascript - Scenario: Creating a taxon with an autogenerated slug for parent + @ui @javascript @api + Scenario: Creating a child taxon with an autogenerated slug Given the store has "Medieval weapons" taxonomy When I want to create a new taxon for "Medieval weapons" And I specify its code as "SIEGE_ENGINES" @@ -34,7 +34,7 @@ Feature: Editing taxon's slug And I add it Then this taxon slug should be "medieval-weapons/siege-engines" - @ui @javascript + @ui @javascript @api Scenario: Creating a child taxon with autogenerated slug by setting the parent Given the store has "Medieval weapons" taxonomy When I want to create a new taxon @@ -44,8 +44,8 @@ Feature: Editing taxon's slug And I add it Then this taxon slug should be "medieval-weapons/pikes" - @ui - Scenario: Creating a taxon with a custom slug for parent + @ui @api + Scenario: Creating a child taxon with a custom slug Given the store has "Medieval weapons" taxonomy When I want to create a new taxon for "Medieval weapons" And I specify its code as "SIEGE_ENGINES" @@ -54,13 +54,13 @@ Feature: Editing taxon's slug And I add it Then this taxon slug should be "medieval-weapons/siege" - @ui + @ui @no-api Scenario: Seeing disabled slug field when editing a taxon Given the store has "Medieval weapons" taxonomy When I want to modify the "Medieval weapons" taxon Then the slug field should not be editable - @ui @javascript + @ui @javascript @api Scenario: Prevent from editing the slug while changing a taxon name Given the store has "Medieval weapons" taxonomy When I want to modify the "Medieval weapons" taxon @@ -68,7 +68,7 @@ Feature: Editing taxon's slug And I save my changes Then the slug of the "Renaissance weapons" taxon should still be "medieval-weapons" - @ui @javascript + @ui @javascript @api Scenario: Prevent from editing the slug while setting the taxon's parent Given the store has "Medieval weapons" taxonomy And the store has "Pikes" taxonomy @@ -77,7 +77,7 @@ Feature: Editing taxon's slug And I save my changes Then the slug of the "Pikes" taxon should still be "pikes" - @ui @javascript + @ui @javascript @api Scenario: Prevent from editing the slug while changing the taxon's parent Given the store has "Medieval weapons" taxonomy Given the store has "Renaissance weapons" taxonomy @@ -87,8 +87,8 @@ Feature: Editing taxon's slug And I save my changes Then the slug of the "Pikes" taxon should still be "medieval-weapons/pikes" - @ui @javascript - Scenario: Automatically changing a taxon's slug while editing a taxon's name + @ui @javascript @api + Scenario: Automatically changing a root taxon's slug while editing a taxon's name Given the store has "Medieval weapons" taxonomy When I want to modify the "Medieval weapons" taxon And I enable slug modification @@ -96,8 +96,18 @@ Feature: Editing taxon's slug And I save my changes Then the slug of the "Renaissance weapons" taxon should be "renaissance-weapons" - @ui @javascript - Scenario: Manually changing a taxon's slug while editing a taxon's name + @ui @javascript @api + Scenario: Automatically changing a child taxon's slug while editing a taxon's name + Given the store has "Medieval weapons" taxonomy + And the "Medieval weapons" taxon has child taxon "Pikes" + When I want to modify the "Pikes" taxon + And I enable slug modification + And I rename it to "Javelins" in "English (United States)" + And I save my changes + Then the slug of the "Javelins" taxon should be "medieval-weapons/javelins" + + @ui @javascript @api + Scenario: Manually changing a root taxon's slug while editing a taxon's name Given the store has "Medieval weapons" taxonomy When I want to modify the "Medieval weapons" taxon And I enable slug modification @@ -106,7 +116,7 @@ Feature: Editing taxon's slug And I save my changes Then the slug of the "Renaissance weapons" taxon should be "renaissance" - @ui @javascript + @ui @javascript @api Scenario: Automatically changing a child taxon's slug when changing the parent Given the store has "Renaissance weapons" taxonomy And the store has "Medieval weapons" taxonomy @@ -117,7 +127,7 @@ Feature: Editing taxon's slug And I save my changes Then the slug of the "Pikes" taxon should be "renaissance-weapons/pikes" - @ui @javascript + @ui @javascript @api Scenario: Manually changing a child taxon's slug when changing the parent Given the store has "Renaissance weapons" taxonomy And the store has "Medieval weapons" taxonomy @@ -128,3 +138,14 @@ Feature: Editing taxon's slug And I set its slug to "pikes" in "English (United States)" And I save my changes Then the slug of the "Pikes" taxon should be "pikes" + + @ui @javascript @api + Scenario: Manually changing a child taxon's slug while editing a taxon's name + Given the store has "Medieval weapons" taxonomy + And the "Medieval weapons" taxon has child taxon "Pikes" + When I want to modify the "Pikes" taxon + And I enable slug modification + And I rename it to "Javelins" in "English (United States)" + And I set its slug to "javelins" in "English (United States)" + And I save my changes + Then the slug of the "Javelins" taxon should be "javelins" diff --git a/features/taxonomy/managing_taxons/editing_taxon_slug_in_multiple_locales.feature b/features/taxonomy/managing_taxons/editing_taxon_slug_in_multiple_locales.feature index c7772748cb..f65880cd0f 100644 --- a/features/taxonomy/managing_taxons/editing_taxon_slug_in_multiple_locales.feature +++ b/features/taxonomy/managing_taxons/editing_taxon_slug_in_multiple_locales.feature @@ -9,7 +9,7 @@ Feature: Editing taxon's slug in multiple locales And the store is also available in "Polish (Poland)" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Creating a root taxon with an autogenerated slug When I want to create a new taxon And I specify its code as "MEDIEVAL_WEAPONS" @@ -19,7 +19,7 @@ Feature: Editing taxon's slug in multiple locales Then this taxon should have slug "medieval-weapons" in "English (United States)" And this taxon should have slug "bronie-sredniowieczne" in "Polish (Poland)" - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Creating a child taxon with an autogenerated slug Given the store has taxonomy named "Medieval weapons" in "English (United States)" locale and "Bronie średniowieczne" in "Polish (Poland)" locale When I want to create a new taxon for "Medieval weapons" @@ -30,7 +30,7 @@ Feature: Editing taxon's slug in multiple locales Then this taxon should have slug "medieval-weapons/siege-engines" in "English (United States)" And this taxon should have slug "bronie-sredniowieczne/machiny-obleznicze" in "Polish (Poland)" - @ui + @ui @api Scenario: Creating a root taxon with a custom slug When I want to create a new taxon And I specify its code as "MEDIEVAL_WEAPONS" @@ -42,14 +42,14 @@ Feature: Editing taxon's slug in multiple locales Then this taxon should have slug "mw" in "English (United States)" And this taxon should have slug "bs" in "Polish (Poland)" - @ui + @ui @no-api Scenario: Seeing disabled slug field when editing a taxon Given the store has taxonomy named "Medieval weapons" in "English (United States)" locale and "Bronie średniowieczne" in "Polish (Poland)" locale When I want to modify the "Medieval weapons" taxon Then the slug field should not be editable in "English (United States)" Then the slug field should also not be editable in "Polish (Poland)" - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Prevent from editing a slug while changing a taxon name Given the store has taxonomy named "Medieval weapons" in "English (United States)" locale and "Bronie średniowieczne" in "Polish (Poland)" locale When I want to modify the "Medieval weapons" taxon @@ -59,7 +59,7 @@ Feature: Editing taxon's slug in multiple locales Then this taxon should have slug "medieval-weapons" in "English (United States)" Then this taxon should have slug "bronie-sredniowieczne" in "Polish (Poland)" - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Automatically changing a taxon's slug while editing a taxon's name Given the store has taxonomy named "Medieval weapons" in "English (United States)" locale and "Bronie średniowieczne" in "Polish (Poland)" locale When I want to modify the "Medieval weapons" taxon @@ -71,7 +71,7 @@ Feature: Editing taxon's slug in multiple locales Then this taxon should have slug "renaissance-weapons" in "English (United States)" Then this taxon should have slug "bronie-renesansowe" in "Polish (Poland)" - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Manually changing a taxon's slug while editing a taxon's name Given the store has taxonomy named "Medieval weapons" in "English (United States)" locale and "Bronie średniowieczne" in "Polish (Poland)" locale When I want to modify the "Medieval weapons" taxon diff --git a/features/taxonomy/managing_taxons/removing_images_of_taxon.feature b/features/taxonomy/managing_taxons/removing_images_of_taxon.feature index d1ddf6fb40..d1c4a3f595 100644 --- a/features/taxonomy/managing_taxons/removing_images_of_taxon.feature +++ b/features/taxonomy/managing_taxons/removing_images_of_taxon.feature @@ -9,54 +9,54 @@ Feature: Removing images of an existing taxon And the store classifies its products as "T-Shirts" And I am logged in as an administrator - @ui @javascript + @ui @javascript @api Scenario: Removing a single image of a taxon Given the "T-Shirts" taxon has an image "t-shirts.jpg" with "banner" type When I want to modify the "T-Shirts" taxon And I remove an image with "banner" type - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this taxon should not have any images - @ui @javascript + @ui @javascript @api Scenario: Removing all images of a taxon Given the "T-Shirts" taxon has an image "t-shirts.jpg" with "banner" type And the "T-Shirts" taxon also has an image "t-shirts.jpg" with "thumbnail" type When I want to modify the "T-Shirts" taxon And I remove an image with "banner" type And I also remove an image with "thumbnail" type - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this taxon should not have any images - @ui @mink:chromedriver + @ui @mink:chromedriver @api Scenario: Removing only one image of a taxon Given the "T-Shirts" taxon has an image "t-shirts.jpg" with "banner" type And the "T-Shirts" taxon also has an image "t-shirts.jpg" with "thumbnail" type When I want to modify the "T-Shirts" taxon And I remove an image with "banner" type - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this taxon should have an image with "thumbnail" type But this taxon should not have any images with "banner" type - @ui @javascript - Scenario: Removing only one image of a simple product when all images have same type + @ui @javascript @api + Scenario: Removing only one image of a taxon when all images have same type Given the "T-Shirts" taxon has an image "t-shirts.jpg" with "banner" type And the "T-Shirts" taxon also has an image "mugs.jpg" with "banner" type When I want to modify the "T-Shirts" taxon And I remove the first image - And I save my changes - Then I should be notified that it has been successfully edited + And I save my changes to the images + Then I should be notified that the changes have been successfully applied And this taxon should have only one image - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Adding multiple images and removing a single image of a taxon When I want to modify the "T-Shirts" taxon And I attach the "t-shirts.jpg" image with "banner" type And I attach the "t-shirts.jpg" image with "thumbnail" type And I remove the first image - And I save my changes + And I save my changes to the images Then I should be notified that it has been successfully edited And this taxon should have an image with "thumbnail" type But this taxon should not have any images with "banner" type diff --git a/features/taxonomy/managing_taxons/reordering_taxon.feature b/features/taxonomy/managing_taxons/reordering_taxon.feature index 6c1772a7b0..19b7812645 100644 --- a/features/taxonomy/managing_taxons/reordering_taxon.feature +++ b/features/taxonomy/managing_taxons/reordering_taxon.feature @@ -8,7 +8,7 @@ Feature: Reordering taxons Given the store classifies its products as "T-Shirts", "Watches", "Belts" and "Wallets" And I am logged in as an administrator - @ui @javascript + @ui @javascript @no-api Scenario: Moving up the taxon on list When I want to see all taxons in store And I move up "Watches" taxon @@ -16,7 +16,7 @@ Feature: Reordering taxons And I should see the taxon named "T-Shirts" in the list But the first taxon on the list should be "Watches" - @ui @javascript + @ui @javascript @no-api Scenario: Moving down the taxon on list When I want to see all taxons in store And I move down "T-Shirts" taxon @@ -24,21 +24,21 @@ Feature: Reordering taxons And I should see the taxon named "Watches" in the list But the first taxon on the list should be "Watches" - @ui @javascript + @ui @javascript @no-api Scenario: Moving up the first taxon on list When I want to see all taxons in store And I move up "T-Shirts" taxon Then I should see 4 taxons on the list And the first taxon on the list should be "T-Shirts" - @ui @javascript + @ui @javascript @no-api Scenario: Moving down the last taxon on list When I want to see all taxons in store And I move down "Wallets" taxon Then I should see 4 taxons on the list And the last taxon on the list should be "Wallets" - @ui @javascript @todo + @ui @javascript @no-api @todo Scenario: Changing order of the taxon on list When I want to see all taxons in store And I move down "T-Shirts" taxon diff --git a/features/taxonomy/managing_taxons/taxon_unique_code_validation.feature b/features/taxonomy/managing_taxons/taxon_unique_code_validation.feature index 7607525e28..fb71180ba9 100644 --- a/features/taxonomy/managing_taxons/taxon_unique_code_validation.feature +++ b/features/taxonomy/managing_taxons/taxon_unique_code_validation.feature @@ -9,7 +9,7 @@ Feature: Taxon unique code validation And the store classifies its products as "T-Shirts" And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add taxon with taken code When I want to create a new taxon And I specify its code as "t_shirts" diff --git a/features/taxonomy/managing_taxons/taxon_validation.feature b/features/taxonomy/managing_taxons/taxon_validation.feature index 2899f4173b..c60cfdff8a 100644 --- a/features/taxonomy/managing_taxons/taxon_validation.feature +++ b/features/taxonomy/managing_taxons/taxon_validation.feature @@ -8,7 +8,7 @@ Feature: Taxon validation Given the store is available in "English (United States)" And I am logged in as an administrator - @ui + @ui @api Scenario: Trying to add a taxon without specifying its code When I want to create a new taxon And I do not specify its code @@ -17,7 +17,7 @@ Feature: Taxon validation Then I should be notified that code is required And taxon named "T-Shirts" should not be added - @ui + @ui @api Scenario: Trying to add a taxon without specifying its name When I want to create a new taxon And I specify its code as "t-shirts" @@ -25,7 +25,7 @@ Feature: Taxon validation And I try to add it Then I should be notified that name is required - @ui + @ui @no-api Scenario: Trying to add a taxon without specifying its slug When I want to create a new taxon And I specify its code as "t-shirts" @@ -34,12 +34,12 @@ Feature: Taxon validation And I try to add it Then I should be notified that slug is required - @ui + @ui @api Scenario: Trying to add a taxon with non unique slug Given the store classifies its products as "T-Shirts" When I want to create a new taxon And I specify its code as "t-shirts-2" And I name it "T-Shirts" in "English (United States)" - And I set its slug to "t-shirts" + And I set its slug to "t-shirts" in "English (United States)" And I try to add it Then I should be notified that taxon slug must be unique diff --git a/features/taxonomy/managing_taxons/toggling_taxon.feature b/features/taxonomy/managing_taxons/toggling_taxon.feature index ccbe176309..2bdfa780ce 100644 --- a/features/taxonomy/managing_taxons/toggling_taxon.feature +++ b/features/taxonomy/managing_taxons/toggling_taxon.feature @@ -9,7 +9,7 @@ Feature: Toggling the taxon And the store classifies its products as "T-Shirts" and "Accessories" And I am logged in as an administrator - @ui + @ui @api Scenario: Adding a disabled taxon When I want to create a new taxon And I specify its code as "jeans" @@ -21,7 +21,7 @@ Feature: Toggling the taxon And the "Jeans" taxon should appear in the registry And it should be disabled - @ui + @ui @api Scenario: Enabling a Taxon Given the "T-Shirts" taxon is disabled When I want to modify the "T-Shirts" taxon @@ -30,7 +30,7 @@ Feature: Toggling the taxon Then I should be notified that it has been successfully edited And it should be enabled - @ui + @ui @api Scenario: Disabling a Taxon Given the "T-Shirts" taxon is enabled When I want to modify the "T-Shirts" taxon diff --git a/features/user/managing_administrators/deleting_multiple_administrators.feature b/features/user/managing_administrators/deleting_multiple_administrators.feature index 2fe20f01ec..16c2661b6f 100644 --- a/features/user/managing_administrators/deleting_multiple_administrators.feature +++ b/features/user/managing_administrators/deleting_multiple_administrators.feature @@ -10,7 +10,7 @@ Feature: Deleting multiple administrators And there is also an administrator "watermelon@example.com" And I am logged in as "watermelon@example.com" administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple administrators at once Given I browse administrators And I check the "banana@example.com" administrator diff --git a/features/user/managing_customer_groups/deleting_multiple_customer_groups.feature b/features/user/managing_customer_groups/deleting_multiple_customer_groups.feature index 36c86b1b16..c4b23d1a1d 100644 --- a/features/user/managing_customer_groups/deleting_multiple_customer_groups.feature +++ b/features/user/managing_customer_groups/deleting_multiple_customer_groups.feature @@ -8,7 +8,7 @@ Feature: Deleting multiple customer groups Given the store has customer groups "Retail", "Wholesale" and "General" And I am logged in as an administrator - @ui @mink:chromedriver + @ui @mink:chromedriver @no-api Scenario: Deleting multiple customer groups at once When I browse customer groups And I check the "Retail" customer group diff --git a/features/user/managing_customers/adding_customer.feature b/features/user/managing_customers/adding_customer.feature index 5a7408c08a..0029390f2b 100644 --- a/features/user/managing_customers/adding_customer.feature +++ b/features/user/managing_customers/adding_customer.feature @@ -8,7 +8,7 @@ Feature: Adding a new customer Given the store has a customer group "Retail" And I am logged in as an administrator - @ui + @api @ui Scenario: Adding a new customer When I want to create a new customer And I specify their email as "l.skywalker@gmail.com" @@ -16,7 +16,7 @@ Feature: Adding a new customer Then I should be notified that it has been successfully created And the customer "l.skywalker@gmail.com" should appear in the store - @ui + @api @ui Scenario: Adding a new customer with full details When I want to create a new customer And I specify their first name as "Luke" diff --git a/features/user/managing_customers/adding_customer_account.feature b/features/user/managing_customers/adding_customer_account.feature index 1756707d3a..cab1ba145b 100644 --- a/features/user/managing_customers/adding_customer_account.feature +++ b/features/user/managing_customers/adding_customer_account.feature @@ -7,7 +7,7 @@ Feature: Adding a new customer account Background: Given I am logged in as an administrator - @ui @javascript + @api @ui @javascript Scenario: Adding a new customer with an account When I want to create a new customer account And I specify their email as "l.skywalker@gmail.com" @@ -18,7 +18,7 @@ Feature: Adding a new customer account And the customer "l.skywalker@gmail.com" should appear in the store And the customer "l.skywalker@gmail.com" should have an account created - @ui @javascript + @api @ui @javascript Scenario: Creating an account for existing customer Given the store has customer "Frodo Baggins" with email "f.baggins@example.com" When I want to edit this customer @@ -28,4 +28,4 @@ Feature: Adding a new customer account Then I should be notified that it has been successfully edited And I should not see create account option And the customer "f.baggins@example.com" should appear in the store - And this customer should have an account created + And the customer "f.baggins@example.com" should have an account created diff --git a/features/user/managing_customers/adding_customer_account_after_failed_creation_action.feature b/features/user/managing_customers/adding_customer_account_after_failed_creation_action.feature index d372b2e94c..2aea8c7994 100644 --- a/features/user/managing_customers/adding_customer_account_after_failed_creation_action.feature +++ b/features/user/managing_customers/adding_customer_account_after_failed_creation_action.feature @@ -7,7 +7,7 @@ Feature: Adding a new customer account after failed creation action Background: Given I am logged in as an administrator - @ui @javascript + @api @ui @javascript Scenario: Trying to add new customer with an account without required information When I want to create a new customer account And I choose create account option @@ -19,7 +19,7 @@ Feature: Adding a new customer account after failed creation action And I should be notified that email is required And I should be notified that password is required - @ui @javascript + @api @ui @javascript Scenario: Trying to add new customer with an account without email When I want to create a new customer account And I choose create account option @@ -31,7 +31,7 @@ Feature: Adding a new customer account after failed creation action And I should not be able to select create account option And I should be notified that email is required - @ui @javascript + @api @ui @javascript Scenario: Trying to add new customer without an account without email When I want to create a new customer account And I do not choose create account option @@ -42,7 +42,7 @@ Feature: Adding a new customer account after failed creation action And I should be able to select create account option And I should be notified that email is required - @ui @javascript + @api @ui @javascript Scenario: Trying to add new customer with an account without required information When I want to create a new customer account And I choose create account option diff --git a/features/user/managing_customers/being_able_to_create_account_for_existing_customer.feature b/features/user/managing_customers/being_able_to_create_account_for_existing_customer.feature index 06a5bb101a..5897ab42d0 100644 --- a/features/user/managing_customers/being_able_to_create_account_for_existing_customer.feature +++ b/features/user/managing_customers/being_able_to_create_account_for_existing_customer.feature @@ -7,7 +7,7 @@ Feature: Create account option availability Background: And I am logged in as an administrator - @ui + @api @ui Scenario: Being able to create an account for created customer When I want to create a new customer And I do not choose create account option @@ -17,7 +17,7 @@ Feature: Create account option availability And I should not be able to specify their password And I should be able to select create account option - @ui @javascript + @ui @javascript @no-api Scenario: Not seeing create account option after adding customer with account When I want to create a new customer account And I choose create account option diff --git a/features/user/managing_customers/browsing_customers.feature b/features/user/managing_customers/browsing_customers.feature index 78601a1559..db9a0e0c29 100644 --- a/features/user/managing_customers/browsing_customers.feature +++ b/features/user/managing_customers/browsing_customers.feature @@ -10,7 +10,7 @@ Feature: Browsing customers And the store has customer "l.skywalker@example.com" And I am logged in as an administrator - @ui + @api @ui Scenario: Browsing customers in store When I want to see all customers in store Then I should see 3 customers in the list diff --git a/features/user/managing_customers/browsing_orders_of_customer.feature b/features/user/managing_customers/browsing_orders_of_customer.feature index 3d31fc6368..36da86b42c 100644 --- a/features/user/managing_customers/browsing_orders_of_customer.feature +++ b/features/user/managing_customers/browsing_orders_of_customer.feature @@ -17,17 +17,17 @@ Feature: Browsing orders of a customer And the customer chose "Free" shipping method to "United States" with "Cash on Delivery" payment And I am logged in as an administrator - @ui + @api @ui Scenario: Browsing orders of a specific customer in the list When I browse orders of a customer "logan@wolverine.com" Then I should see a single order in the list And I should see the order with number "#00000007" in the list And I should not see the order with number "#00000008" in the list - @ui - Scenario: Browsing orders of a specific customer in the list + @api @ui + Scenario: Sorting orders of customer by channel When I browse orders of a customer "logan@wolverine.com" - And I sort them by channel + And I sort the orders ascending by channel Then I should see a single order in the list And I should see the order with number "#00000007" in the list And I should not see the order with number "#00000008" in the list diff --git a/features/user/managing_customers/changing_customer_email.feature b/features/user/managing_customers/changing_customer_email.feature index 5609b526f1..02317b7674 100644 --- a/features/user/managing_customers/changing_customer_email.feature +++ b/features/user/managing_customers/changing_customer_email.feature @@ -9,7 +9,7 @@ Feature: Changing an email of an existing customer And there is a customer "Frodo Baggins" with an email "f.baggins@example.com" and a password "ring" And I am logged in as an administrator - @ui + @api @ui Scenario: Changing an email of an existing customer When I want to edit this customer And I change their email to "strawberry@example.com" diff --git a/features/user/managing_customers/customer_filtration/filtering_customers_by_groups.feature b/features/user/managing_customers/customer_filtration/filtering_customers_by_groups.feature new file mode 100644 index 0000000000..e94cd200fe --- /dev/null +++ b/features/user/managing_customers/customer_filtration/filtering_customers_by_groups.feature @@ -0,0 +1,30 @@ +@managing_customers +Feature: Filtering customers by groups + In order to quickly find customers belonging to specific groups + As an Administrator + I want to be able to filter customers on the list + + Background: + Given the store has a customer group "Retail" + And the store has a customer group "Wholesale" + And the store has customer "f.baggins@example.com" + And the store has customer "g.bespoke@example.com" + And this customer belongs to group "Retail" + And the store has customer "l.abhorsen@example.com" + And this customer belongs to group "Wholesale" + And I am logged in as an administrator + + @api @ui @javascript + Scenario: Filtering customers by a group + When I want to see all customers in store + And I filter by group "Retail" + Then I should see a single customer on the list + And I should see the customer "g.bespoke@example.com" in the list + + @api @ui @mink:chromedriver + Scenario: Filtering customers by multiple groups + When I want to see all customers in store + And I filter by groups "Retail" and "Wholesale" + Then I should see 2 customers in the list + And I should see the customer "l.abhorsen@example.com" in the list + And I should see the customer "g.bespoke@example.com" in the list diff --git a/features/user/managing_customers/customer_unique_email_validation.feature b/features/user/managing_customers/customer_unique_email_validation.feature index 155344dd5b..582e06294b 100644 --- a/features/user/managing_customers/customer_unique_email_validation.feature +++ b/features/user/managing_customers/customer_unique_email_validation.feature @@ -7,7 +7,7 @@ Feature: Customer uniqueness of email validation Background: Given I am logged in as an administrator - @ui + @api @ui Scenario: Trying to add a new customer with taken email Given the store has customer "f.baggins@example.com" When I want to create a new customer diff --git a/features/user/managing_customers/customer_validation.feature b/features/user/managing_customers/customer_validation.feature index bb0b8e8325..f9ff6b850a 100644 --- a/features/user/managing_customers/customer_validation.feature +++ b/features/user/managing_customers/customer_validation.feature @@ -7,7 +7,7 @@ Feature: Customer validation Background: Given I am logged in as an administrator - @ui + @api @ui Scenario: Trying to add a new customer without an email When I want to create a new customer And I specify their first name as "Luke" @@ -15,7 +15,7 @@ Feature: Customer validation And I try to add them Then I should be notified that email is required - @ui + @api @ui Scenario: Trying to specify too short first name for an existing customer Given the store has customer "l.skywalker@gmail.com" When I want to edit this customer @@ -24,7 +24,7 @@ Feature: Customer validation Then I should be notified that first name should be at least 2 characters long And the customer "l.skywalker@gmail.com" should still have an empty first name - @ui + @api @ui Scenario: Trying to specify too short last name for an existing customer Given the store has customer "l.skywalker@gmail.com" with first name "Luke" When I want to edit this customer @@ -33,7 +33,7 @@ Feature: Customer validation Then I should be notified that last name should be at least 2 characters long And the customer "l.skywalker@gmail.com" should still have an empty last name - @ui + @api @ui Scenario: Trying to remove email from an existing customer Given the store has customer "l.skywalker@gmail.com" When I want to edit this customer @@ -42,7 +42,7 @@ Feature: Customer validation Then I should be notified that email is required And the customer "l.skywalker@gmail.com" should still have this email - @ui + @api @ui Scenario: Trying to create customer with wrong email format When I want to create a new customer And I specify their email as "wrongemail" @@ -50,7 +50,7 @@ Feature: Customer validation Then I should be notified that email is not valid And the customer with email "wrongemail" should not appear in the store - @ui + @api @ui Scenario: Trying to create customer with wrong email format in strict mode When I want to create a new customer And I specify their email as "wrongemail@example..com" diff --git a/features/user/managing_customers/editing_customer.feature b/features/user/managing_customers/editing_customer.feature index 4aa0b73146..1fe8b28fc2 100644 --- a/features/user/managing_customers/editing_customer.feature +++ b/features/user/managing_customers/editing_customer.feature @@ -7,7 +7,7 @@ Feature: Editing a customer Background: Given I am logged in as an administrator - @ui + @api @ui Scenario: Changing first and last name of an existing customer Given the store has customer "Frodo Baggins" with email "f.baggins@example.com" When I want to edit this customer @@ -17,7 +17,7 @@ Feature: Editing a customer Then I should be notified that it has been successfully edited And this customer with name "Jon Snow" should appear in the store - @ui + @api @ui Scenario: Removing first and last name from an existing customer Given the store has customer "Luke Skywalker" with email "l.skywalker@gmail.com" When I want to edit this customer @@ -28,7 +28,7 @@ Feature: Editing a customer And this customer should have an empty first name And this customer should have an empty last name - @ui + @api @ui Scenario: Making an existing customer subscribed to the newsletter Given the store has customer "Mike Ross" with email "ross@teammike.com" When I want to edit this customer @@ -37,7 +37,7 @@ Feature: Editing a customer Then I should be notified that it has been successfully edited And this customer should be subscribed to the newsletter - @ui + @api @ui Scenario: Selecting a group for an existing customer Given the store has a customer group "Retail" And the store has customer "Mike Ross" with email "ross@teammike.com" diff --git a/features/user/managing_customers/seeing_customer_details.feature b/features/user/managing_customers/seeing_customer_details.feature index 7503b50951..e2919aebfd 100644 --- a/features/user/managing_customers/seeing_customer_details.feature +++ b/features/user/managing_customers/seeing_customer_details.feature @@ -9,44 +9,44 @@ Feature: Seeing customer's details And the store has a customer group Retail And the store has customer "f.baggins@shire.me" with name "Frodo Baggins" and phone number "666777888" since "2011-01-10 21:00" - @ui + @api @ui Scenario: Seeing customer's basic information When I view details of the customer "f.baggins@shire.me" - Then his name should be "Frodo Baggins" - And he should be registered since "2011-01-10 21:00" - And his email should be "f.baggins@shire.me" - And his phone number should be "666777888" + Then their name should be "Frodo Baggins" + And he should be registered since "2011-01-10 21:00:00" + And their email should be "f.baggins@shire.me" + And their phone number should be "666777888" - @ui + @api @ui Scenario: Seeing customer's default address Given the store operates in "New Zealand" And their default address is "Hobbiton", "Bag End", "1", "New Zealand" for "Frodo Baggins" When I view details of the customer "f.baggins@shire.me" - Then his default address should be "Frodo Baggins, Bag End, Hobbiton, NEW ZEALAND 1" + Then their default address should be "Frodo" "Baggins", "Bag End", "1" "Hobbiton", "New Zealand" - @ui + @api @ui Scenario: Seeing information about no existing account for a given customer When I view details of the customer "f.baggins@shire.me" Then I should see information about no existing account for this customer - @ui + @api @ui Scenario: Seeing information about subscription to the newsletter Given the customer subscribed to the newsletter When I view details of the customer "f.baggins@shire.me" Then I should see that this customer is subscribed to the newsletter - @ui + @api @ui Scenario: Seeing information about customer groups Given the customer belongs to group "Retail" When I view details of the customer "f.baggins@shire.me" Then this customer should have "Retail" as their group - @ui + @api @ui Scenario: Not Seeing information about email verification for not registered customer When I view details of the customer "f.baggins@shire.me" Then I should not see information about email verification - @ui + @api @ui Scenario: Seeing information about verified email for registered customer Given there is a customer "Legolas Sindar" identified by an email "legolas@woodland.rm" and a password "takingTheHobbitsToIsengard" And this customer verified their email diff --git a/features/user/managing_customers/seeing_orders_statistics_on_customer_details_page.feature b/features/user/managing_customers/seeing_orders_statistics_on_customer_details_page.feature index 1913587bda..e7d327024a 100644 --- a/features/user/managing_customers/seeing_orders_statistics_on_customer_details_page.feature +++ b/features/user/managing_customers/seeing_orders_statistics_on_customer_details_page.feature @@ -7,8 +7,8 @@ Feature: Seeing customer's orders' statistics Background: Given the store operates on a channel named "Web-US" in "USD" currency And the store also operates on another channel named "Web-UK" in "GBP" currency - And the store has a product "Onion" priced at "$200" in "Web-US" channel - And this product is also priced at "£100" in "Web-UK" channel + And the store has a product "Onion" priced at "$200.00" in "Web-US" channel + And this product is also priced at "£100.00" in "Web-UK" channel And the store has customer "lirael.clayr@abhorsen.ok" And I am logged in as an administrator diff --git a/features/user/managing_customers/seeing_province_created_manually_on_customer_details_page.feature b/features/user/managing_customers/seeing_province_created_manually_on_customer_details_page.feature index 245567007a..44b7c983e7 100644 --- a/features/user/managing_customers/seeing_province_created_manually_on_customer_details_page.feature +++ b/features/user/managing_customers/seeing_province_created_manually_on_customer_details_page.feature @@ -4,7 +4,7 @@ Feature: Seeing a province on customer's details As an Administrator I want to be able to see specific customer's page with provinces in the addresses - @ui + @api @ui Scenario: Seeing customer's addresses Given the store operates in "United Kingdom" And the store has customer "f.baggins@shire.me" with name "Frodo Baggins" since "2011-01-10 21:00" diff --git a/features/user/managing_customers/toggling_customer_account.feature b/features/user/managing_customers/toggling_customer_account.feature index 9c7a45f7ce..d2844b4e54 100644 --- a/features/user/managing_customers/toggling_customer_account.feature +++ b/features/user/managing_customers/toggling_customer_account.feature @@ -7,7 +7,7 @@ Feature: Toggling a customer account Background: Given I am logged in as an administrator - @ui + @api @ui Scenario: Enabling a customer account Given there is disabled customer account "f.baggins@example.com" with password "psw" When I want to enable "f.baggins@example.com" @@ -16,7 +16,7 @@ Feature: Toggling a customer account Then I should be notified that it has been successfully edited And this customer should be enabled - @ui + @api @ui Scenario: Disabling a customer account Given there is enabled customer account "f.baggins@example.com" with password "psw" When I want to disable "f.baggins@example.com" diff --git a/features/user/managing_customers/verifying_customer_account.feature b/features/user/managing_customers/verifying_customer_account.feature index 5ae4d4a27a..3060e99fb7 100644 --- a/features/user/managing_customers/verifying_customer_account.feature +++ b/features/user/managing_customers/verifying_customer_account.feature @@ -7,7 +7,7 @@ Feature: Toggling a customer account Background: Given I am logged in as an administrator - @ui + @api @ui Scenario: Verifying customer account Given there is enabled customer account "f.baggins@example.com" with password "psw" When I want to verify "f.baggins@example.com" diff --git a/features/user/managing_users/changing_shop_user_password.feature b/features/user/managing_users/changing_shop_user_password.feature index 4b1aaffcef..b05422d149 100644 --- a/features/user/managing_users/changing_shop_user_password.feature +++ b/features/user/managing_users/changing_shop_user_password.feature @@ -9,7 +9,7 @@ Feature: Changing shop user's password And there is a user "kibsoon@example.com" identified by "goodGuy" And I am logged in as an administrator - @ui + @api @ui Scenario: Changing a password of a shop user When I change the password of user "kibsoon@example.com" to "veryGoodGuy" Then I should be notified that it has been successfully edited diff --git a/features/user/managing_users/deleting_account.feature b/features/user/managing_users/deleting_account.feature index 1c075b00ba..d2fda525ff 100644 --- a/features/user/managing_users/deleting_account.feature +++ b/features/user/managing_users/deleting_account.feature @@ -9,13 +9,13 @@ Feature: Deleting the customer account And there is a user "theodore@example.com" identified by "pswd" And I am logged in as an administrator - @ui + @api @ui Scenario: Deleting account should not delete customer details When I delete the account of "theodore@example.com" user Then the user account should be deleted But the customer with this email should still exist - @ui + @api @ui Scenario: A customer with no user cannot be deleted Given the account of "theodore@example.com" was deleted Then I should not be able to delete it again diff --git a/package.json b/package.json index 68026bf804..328518adc7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sylius-ui/frontend", - "version": "1.0.1", + "version": "1.0.3", "repository": { "type": "git", "url": "git+https://github.com/Sylius/Sylius.git" @@ -62,7 +62,7 @@ "yargs": "^17.5.1" }, "engines": { - "node": "^14 || ^16 || ^18 || ^20" + "node": "^18 || ^20" }, "engineStrict": true } diff --git a/phparkitect.php b/phparkitect.php index e3cb723069..be09fb5922 100644 --- a/phparkitect.php +++ b/phparkitect.php @@ -6,6 +6,7 @@ use Arkitect\ClassSet; use Arkitect\CLI\Config; use Arkitect\Expression\ForClasses\Extend; use Arkitect\Expression\ForClasses\HaveNameMatching; +use Arkitect\Expression\ForClasses\IsNotAbstract; use Arkitect\Expression\ForClasses\IsFinal; use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces; use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces; @@ -14,27 +15,95 @@ use PhpSpec\ObjectBehavior; return static function (Config $config): void { - $srcClassSet = ClassSet::fromDir(__DIR__.'/src'); + $specClassSet = ClassSet::fromDir(__DIR__ . '/src/Sylius/{Behat,Component/*,Bundle/*}/spec'); - $rules = []; + $config->add( + $specClassSet, + Rule::allClasses() + ->that(new Extend(ObjectBehavior::class)) + ->should(new HaveNameMatching('*Spec')) + ->because('Specifications should follow PHPSpec naming convention') + , + Rule::allClasses() + ->that(new Extend(ObjectBehavior::class)) + ->andThat(new IsNotAbstract()) + ->should(new IsFinal()) + ->because('Specifications should not be extendable') + , + ); - $rules[] = Rule::allClasses() - ->that(new ResideInOneOfTheseNamespaces('Sylius\Component')) - ->should(new NotDependsOnTheseNamespaces('Sylius\Bundle')) - ->because('Sylius components should be stand-alone') - ; + $testsClassSet = ClassSet::fromDir(__DIR__ . '{/tests,/src/Sylius/Bundle/*/Tests}'); - $rules[] = Rule::allClasses() - ->that(new Extend(ObjectBehavior::class)) - ->should(new HaveNameMatching('*Spec')) - ->because('This is a convention from PHPSpec') - ; + $config->add( + $testsClassSet, + Rule::allClasses() + ->that(new HaveNameMatching('*Test$')) + ->should(new IsFinal()) + ->because('Tests should not be extendable') + , + ); - $rules[] = Rule::allClasses() - ->that(new Extend(ObjectBehavior::class)) - ->should(new IsFinal()) - ->because('Specifications should not be extendable') - ; + $separationClassSet = ClassSet::fromDir(__DIR__ . '/src/Sylius/{Component,Bundle}'); - $config->add($srcClassSet, ...$rules); + $config->add( + $separationClassSet, + Rule::allClasses() + ->that(new ResideInOneOfTheseNamespaces('Sylius\Component')) + ->should(new NotDependsOnTheseNamespaces('Sylius\Bundle')) + ->because('Components should not depend on bundles') + , + Rule::allClasses() + ->except('Sylius\Component\Core') + ->that(new ResideInOneOfTheseNamespaces('Sylius\Component')) + ->should(new NotDependsOnTheseNamespaces('Sylius\Component\Core')) + ->because('Stand-alone components should not depend on Core') + , + Rule::allClasses() + ->except( + 'Sylius\Bundle\AdminBundle', + 'Sylius\Bundle\ApiBundle', + 'Sylius\Bundle\CoreBundle', + 'Sylius\Bundle\PayumBundle', + 'Sylius\Bundle\ShopBundle', + ) + ->that(new ResideInOneOfTheseNamespaces('Sylius\Bundle')) + ->should(new NotDependsOnTheseNamespaces('Sylius\Component\Core')) + ->because('Stand-alone bundles should not depend on Core') + , + Rule::allClasses() + ->except( + 'Sylius\Bundle\AdminBundle', + 'Sylius\Bundle\ApiBundle', + 'Sylius\Bundle\CoreBundle', + 'Sylius\Bundle\ShopBundle', + ) + ->that(new ResideInOneOfTheseNamespaces('Sylius\Bundle')) + ->should(new NotDependsOnTheseNamespaces('Sylius\Bundle\CoreBundle')) + ->because('Stand-alone bundles should not depend on CoreBundle') + , + Rule::allClasses() + ->that(new ResideInOneOfTheseNamespaces('Sylius\Bundle\ShopBundle')) + ->should(new NotDependsOnTheseNamespaces( + 'Sylius\Bundle\AdminBundle', + 'Sylius\Bundle\ApiBundle', + )) + ->because('Shop should not depend on Admin and Api') + , + Rule::allClasses() + ->that(new ResideInOneOfTheseNamespaces('Sylius\Bundle\AdminBundle')) + ->should(new NotDependsOnTheseNamespaces( + 'Sylius\Bundle\ApiBundle', + 'Sylius\Bundle\ShopBundle', + )) + ->because('Admin should not depend on Shop and Api') + , + Rule::allClasses() + ->that(new ResideInOneOfTheseNamespaces('Sylius\Bundle\ApiBundle')) + ->should(new NotDependsOnTheseNamespaces( + 'Sylius\Bundle\AdminBundle', + 'Sylius\Bundle\ShopBundle', + )) + ->because('Api should not depend on Admin and Shop') + , + ); }; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b4239f8a0a..7325a5f3e6 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -66,25 +66,35 @@ parameters: path: src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ProvinceAddressConstraintValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\AddressingBundle\\\\Validator\\\\Constraints\\\\ProvinceAddressConstraintValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Action\\\\Account\\\\RenderResetPasswordPageAction\\:\\:__construct\\(\\) has parameter \\$userRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ProvinceAddressConstraintValidator.php + path: src/Sylius/Bundle/AdminBundle/Action/Account/RenderResetPasswordPageAction.php - - message: "#^Method Sylius\\\\Bundle\\\\AddressingBundle\\\\Validator\\\\Constraints\\\\UniqueProvinceCollectionValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Action\\\\RemoveAvatarAction\\:\\:__construct\\(\\) has parameter \\$avatarRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\AvatarImageRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/AddressingBundle/Validator/Constraints/UniqueProvinceCollectionValidator.php + path: src/Sylius/Bundle/AdminBundle/Action/RemoveAvatarAction.php - - message: "#^Method Sylius\\\\Bundle\\\\AddressingBundle\\\\Validator\\\\Constraints\\\\ZoneCannotContainItselfValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Action\\\\ResendOrderConfirmationEmailAction\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ZoneCannotContainItselfValidator.php + path: src/Sylius/Bundle/AdminBundle/Action/ResendOrderConfirmationEmailAction.php + + - + message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Action\\\\ResendShipmentConfirmationEmailAction\\:\\:__construct\\(\\) has parameter \\$shipmentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/AdminBundle/Action/ResendShipmentConfirmationEmailAction.php - message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Controller\\\\CustomerStatisticsController\\:\\:__construct\\(\\) has parameter \\$customerRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/AdminBundle/Controller/CustomerStatisticsController.php + - + message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Controller\\\\DashboardController\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/AdminBundle/Controller/DashboardController.php + - message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Controller\\\\RedirectHandler\\:\\:redirectToRoute\\(\\) has parameter \\$parameters with no value type specified in iterable type array\\.$#" count: 1 @@ -176,14 +186,14 @@ parameters: path: src/Sylius/Bundle/AdminBundle/Menu/PromotionUpdateMenuBuilder.php - - message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Provider\\\\StatisticsDataProvider\\:\\:getRawData\\(\\) return type has no value type specified in iterable type array\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Twig\\\\ChannelNameExtension\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProvider.php + path: src/Sylius/Bundle/AdminBundle/Twig/ChannelNameExtension.php - - message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Provider\\\\StatisticsDataProviderInterface\\:\\:getRawData\\(\\) return type has no value type specified in iterable type array\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Twig\\\\ChannelsCurrenciesExtension\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProviderInterface.php + path: src/Sylius/Bundle/AdminBundle/Twig/ChannelsCurrenciesExtension.php - message: "#^Method Sylius\\\\Bundle\\\\AdminBundle\\\\Twig\\\\ChannelsCurrenciesExtension\\:\\:getAllCurrencies\\(\\) return type has no value type specified in iterable type array\\.$#" @@ -220,81 +230,6 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/ApiPlatform/ApiResourceConfigurationMergerInterface.php - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$formats with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthAuthorizationUrl with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthClientId with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthClientSecret with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthEnabled with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthFlow with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthScopes with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthTokenUrl with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__construct\\(\\) has parameter \\$oauthType with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:__invoke\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:getContext\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:getPathAndMethod\\(\\) has parameter \\$swaggerData with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:getPathAndMethod\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Property Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:\\$formats type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - - - message: "#^Property Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Bundle\\\\Action\\\\SwaggerUiAction\\:\\:\\$formatsProvider has no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Bridge\\\\Symfony\\\\Routing\\\\RouteNameResolver\\:\\:isSameSubresource\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -330,6 +265,76 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Factory/MergingExtractorResourceMetadataFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\Merger\\\\LegacyResourceMetadataMerger\\:\\:merge\\(\\) has parameter \\$newMetadata with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/LegacyResourceMetadataMerger.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\Merger\\\\LegacyResourceMetadataMerger\\:\\:merge\\(\\) has parameter \\$oldMetadata with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/LegacyResourceMetadataMerger.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\Merger\\\\LegacyResourceMetadataMerger\\:\\:merge\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/LegacyResourceMetadataMerger.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\Merger\\\\MetadataMergerInterface\\:\\:merge\\(\\) has parameter \\$newMetadata with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/MetadataMergerInterface.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\Merger\\\\MetadataMergerInterface\\:\\:merge\\(\\) has parameter \\$oldMetadata with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/MetadataMergerInterface.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\Merger\\\\MetadataMergerInterface\\:\\:merge\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/MetadataMergerInterface.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\MergingXmlExtractor\\:\\:buildResource\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/MergingXmlExtractor.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\MergingXmlExtractor\\:\\:extractAttributes\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/MergingXmlExtractor.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\MergingXmlExtractor\\:\\:extractOperations\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/MergingXmlExtractor.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\MergingXmlExtractor\\:\\:extractProperties\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/MergingXmlExtractor.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\MergingXmlExtractor\\:\\:getProperties\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/MergingXmlExtractor.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\MergingXmlExtractor\\:\\:getResources\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/MergingXmlExtractor.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\MergingXmlExtractor\\:\\:phpizeContent\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/MergingXmlExtractor.php + + - + message: "#^Property Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\Metadata\\\\MergingXmlExtractor\\:\\:\\$properties type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/MergingXmlExtractor.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\ApiPlatform\\\\ResourceMetadataPropertyValueResolver\\:\\:resolve\\(\\) has parameter \\$childResourceMetadata with no value type specified in iterable type array\\.$#" count: 1 @@ -340,6 +345,21 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/ApiPlatform/ResourceMetadataPropertyValueResolverInteface.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Assigner\\\\OrderPromotionCodeAssigner\\:\\:__construct\\(\\) has parameter \\$promotionCouponRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\PromotionCouponRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Assigner/OrderPromotionCodeAssigner.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Changer\\\\PaymentMethodChanger\\:\\:__construct\\(\\) has parameter \\$paymentMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Changer/PaymentMethodChanger.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Changer\\\\PaymentMethodChanger\\:\\:__construct\\(\\) has parameter \\$paymentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Changer/PaymentMethodChanger.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Command\\\\Account\\\\ChangeShopUserPassword\\:\\:getShopUserId\\(\\) has no return type specified\\.$#" count: 1 @@ -375,11 +395,66 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/Command/ShopUserIdAwareInterface.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\ChangePaymentMethodHandler\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/ChangePaymentMethodHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\ChangeShopUserPasswordHandler\\:\\:__construct\\(\\) has parameter \\$userRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/ChangeShopUserPasswordHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\RegisterShopUserHandler\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/RegisterShopUserHandler.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\RegisterShopUserHandler\\:\\:__construct\\(\\) has parameter \\$shopUserFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/RegisterShopUserHandler.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\RequestResetPasswordTokenHandler\\:\\:__construct\\(\\) has parameter \\$userRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/RequestResetPasswordTokenHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\ResendVerificationEmailHandler\\:\\:__construct\\(\\) has parameter \\$shopUserRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/ResendVerificationEmailHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\SendAccountRegistrationEmailHandler\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountRegistrationEmailHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\SendAccountRegistrationEmailHandler\\:\\:__construct\\(\\) has parameter \\$shopUserRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountRegistrationEmailHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\SendAccountVerificationEmailHandler\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountVerificationEmailHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\SendAccountVerificationEmailHandler\\:\\:__construct\\(\\) has parameter \\$shopUserRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountVerificationEmailHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\SendResetPasswordEmailHandler\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendResetPasswordEmailHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\SendResetPasswordEmailHandler\\:\\:__construct\\(\\) has parameter \\$userRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendResetPasswordEmailHandler.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Account\\\\SendResetPasswordEmailHandler\\:\\:__invoke\\(\\) has no return type specified\\.$#" count: 1 @@ -390,6 +465,66 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/CommandHandler/Account/VerifyCustomerAccountHandler.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\AddItemToCartHandler\\:\\:__construct\\(\\) has parameter \\$cartItemFactory with generic interface Sylius\\\\Component\\\\Core\\\\Factory\\\\CartItemFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/AddItemToCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\AddItemToCartHandler\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/AddItemToCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\AddItemToCartHandler\\:\\:__construct\\(\\) has parameter \\$productVariantRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductVariantRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/AddItemToCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\BlameCartHandler\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/BlameCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\BlameCartHandler\\:\\:__construct\\(\\) has parameter \\$shopUserRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/BlameCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\ChangeItemQuantityInCartHandler\\:\\:__construct\\(\\) has parameter \\$orderItemRepository with generic interface Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderItemRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/ChangeItemQuantityInCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\PickupCartHandler\\:\\:__construct\\(\\) has parameter \\$cartFactory with generic interface Sylius\\\\Bundle\\\\CoreBundle\\\\Factory\\\\OrderFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/PickupCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\PickupCartHandler\\:\\:__construct\\(\\) has parameter \\$cartRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/PickupCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\PickupCartHandler\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/PickupCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\PickupCartHandler\\:\\:__construct\\(\\) has parameter \\$customerRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\CustomerRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/PickupCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Cart\\\\RemoveItemFromCartHandler\\:\\:__construct\\(\\) has parameter \\$orderItemRepository with generic interface Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderItemRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/RemoveItemFromCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Catalog\\\\AddProductReviewHandler\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Catalog/AddProductReviewHandler.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Catalog\\\\AddProductReviewHandler\\:\\:__construct\\(\\) has parameter \\$productReviewFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -400,11 +535,96 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/CommandHandler/Catalog/AddProductReviewHandler.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\ChoosePaymentMethodHandler\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChoosePaymentMethodHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\ChoosePaymentMethodHandler\\:\\:__construct\\(\\) has parameter \\$paymentMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChoosePaymentMethodHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\ChoosePaymentMethodHandler\\:\\:__construct\\(\\) has parameter \\$paymentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChoosePaymentMethodHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\ChooseShippingMethodHandler\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChooseShippingMethodHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\ChooseShippingMethodHandler\\:\\:__construct\\(\\) has parameter \\$shipmentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChooseShippingMethodHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\ChooseShippingMethodHandler\\:\\:__construct\\(\\) has parameter \\$shippingMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShippingMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChooseShippingMethodHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\CompleteOrderHandler\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/CompleteOrderHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\SendOrderConfirmationHandler\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/SendOrderConfirmationHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\SendShipmentConfirmationEmailHandler\\:\\:__construct\\(\\) has parameter \\$shipmentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/SendShipmentConfirmationEmailHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\ShipShipmentHandler\\:\\:__construct\\(\\) has parameter \\$shipmentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ShipShipmentHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\Checkout\\\\UpdateCartHandler\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/UpdateCartHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\CommandHandler\\\\SendContactRequestHandler\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/CommandHandler/SendContactRequestHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Context\\\\TokenValueBasedCartContext\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Context/TokenValueBasedCartContext.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Controller\\\\DeleteOrderItemAction\\:\\:__construct\\(\\) has parameter \\$orderItemRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderItemRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Controller/DeleteOrderItemAction.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Controller\\\\GetProductBySlugAction\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Controller/GetProductBySlugAction.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Controller\\\\Payment\\\\GetPaymentConfiguration\\:\\:__construct\\(\\) has parameter \\$paymentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Controller/Payment/GetPaymentConfiguration.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Controller\\\\UploadAvatarImageAction\\:\\:__construct\\(\\) has parameter \\$avatarImageFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Controller/UploadAvatarImageAction.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Controller\\\\UploadAvatarImageAction\\:\\:__construct\\(\\) has parameter \\$avatarImageRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\AvatarImageRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Controller/UploadAvatarImageAction.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Converter\\\\IriToIdentifierConverter\\:\\:isIdentifier\\(\\) has parameter \\$fieldValue with no type specified\\.$#" count: 1 @@ -495,6 +715,56 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataPersister/MessengerDataPersister.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductAttributeDataPersister\\:\\:persist\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductAttributeDataPersister\\:\\:remove\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductAttributeDataPersister\\:\\:remove\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductAttributeDataPersister\\:\\:supports\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductDataPersister\\:\\:remove\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductDataPersister.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductTaxonDataPersister\\:\\:persist\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductTaxonDataPersister.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductTaxonDataPersister\\:\\:remove\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductTaxonDataPersister.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductTaxonDataPersister\\:\\:remove\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductTaxonDataPersister.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductTaxonDataPersister\\:\\:supports\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductTaxonDataPersister.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ProductVariantDataPersister\\:\\:remove\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataPersister/ProductVariantDataPersister.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataPersister\\\\ShippingMethodDataPersister\\:\\:persist\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -605,6 +875,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataProvider/ChannelsCollectionDataProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\CustomerItemDataProvider\\:\\:__construct\\(\\) has parameter \\$customerRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\CustomerRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/CustomerItemDataProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\CustomerItemDataProvider\\:\\:getItem\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -620,6 +895,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataProvider/CustomerItemDataProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\OrderAdjustmentsSubresourceDataProvider\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/OrderAdjustmentsSubresourceDataProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\OrderAdjustmentsSubresourceDataProvider\\:\\:getSubresource\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -641,24 +921,9 @@ parameters: path: src/Sylius/Bundle/ApiBundle/DataProvider/OrderAdjustmentsSubresourceDataProvider.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\OrderItemAdjustmentsSubresourceDataProvider\\:\\:getSubresource\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\OrderItemItemDataProvider\\:\\:__construct\\(\\) has parameter \\$orderItemRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderItemRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemAdjustmentsSubresourceDataProvider.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\OrderItemAdjustmentsSubresourceDataProvider\\:\\:getSubresource\\(\\) has parameter \\$identifiers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemAdjustmentsSubresourceDataProvider.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\OrderItemAdjustmentsSubresourceDataProvider\\:\\:getSubresource\\(\\) return type has no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemAdjustmentsSubresourceDataProvider.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\OrderItemAdjustmentsSubresourceDataProvider\\:\\:supports\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemAdjustmentsSubresourceDataProvider.php + path: src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemItemDataProvider.php - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\OrderItemItemDataProvider\\:\\:getItem\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" @@ -675,6 +940,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemItemDataProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\OrderItemUnitItemDataProvider\\:\\:__construct\\(\\) has parameter \\$orderItemUnitRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderItemUnitRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemUnitItemDataProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\OrderItemUnitItemDataProvider\\:\\:getItem\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -690,6 +960,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemUnitItemDataProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\PaymentItemDataProvider\\:\\:__construct\\(\\) has parameter \\$paymentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/PaymentItemDataProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\PaymentItemDataProvider\\:\\:getItem\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -705,6 +980,21 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataProvider/PaymentItemDataProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\PaymentMethodsCollectionDataProvider\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/PaymentMethodsCollectionDataProvider.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\PaymentMethodsCollectionDataProvider\\:\\:__construct\\(\\) has parameter \\$paymentMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/PaymentMethodsCollectionDataProvider.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\PaymentMethodsCollectionDataProvider\\:\\:__construct\\(\\) has parameter \\$paymentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/PaymentMethodsCollectionDataProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\PaymentMethodsCollectionDataProvider\\:\\:getCollection\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -725,6 +1015,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataProvider/PaymentMethodsCollectionDataProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\ProductAttributesSubresourceDataProvider\\:\\:__construct\\(\\) has parameter \\$attributeValueRepository with generic interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductAttributeValueRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/ProductAttributesSubresourceDataProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\ProductAttributesSubresourceDataProvider\\:\\:__construct\\(\\) has parameter \\$collectionExtensions with no value type specified in iterable type iterable\\.$#" count: 1 @@ -750,6 +1045,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataProvider/ProductAttributesSubresourceDataProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\ProductItemDataProvider\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/ProductItemDataProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\ProductItemDataProvider\\:\\:getItem\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -765,6 +1065,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataProvider/ProductItemDataProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\ShipmentItemDataProvider\\:\\:__construct\\(\\) has parameter \\$shipmentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/ShipmentItemDataProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\ShipmentItemDataProvider\\:\\:getItem\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -780,6 +1085,16 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/DataProvider/ShipmentItemDataProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\ShippingMethodsCollectionDataProvider\\:\\:__construct\\(\\) has parameter \\$shipmentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/ShippingMethodsCollectionDataProvider.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\ShippingMethodsCollectionDataProvider\\:\\:__construct\\(\\) has parameter \\$shippingMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShippingMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/DataProvider/ShippingMethodsCollectionDataProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\DataProvider\\\\ShippingMethodsCollectionDataProvider\\:\\:getCollection\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -1025,6 +1340,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AddressesExtension.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryCollectionExtension\\\\AvailableProductAssociationsInProductCollectionExtension\\:\\:applyToCollection\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AvailableProductAssociationsInProductCollectionExtension.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryCollectionExtension\\\\CountryCollectionExtension\\:\\:applyToCollection\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -1125,6 +1445,21 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/AddressItemExtension.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\EnabledProductInProductAssociationItemExtension\\:\\:applyToItem\\(\\) has no return type specified\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductInProductAssociationItemExtension.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\EnabledProductInProductAssociationItemExtension\\:\\:applyToItem\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductInProductAssociationItemExtension.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\EnabledProductInProductAssociationItemExtension\\:\\:applyToItem\\(\\) has parameter \\$identifiers with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductInProductAssociationItemExtension.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\EnabledProductVariantItemExtension\\:\\:applyToItem\\(\\) has no return type specified\\.$#" count: 1 @@ -1140,6 +1475,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductVariantItemExtension.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\OrderShopUserItemExtension\\:\\:__construct\\(\\) has parameter \\$nonFilteredCartAllowedOperations with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\OrderShopUserItemExtension\\:\\:applyToItem\\(\\) has no return type specified\\.$#" count: 1 @@ -1155,6 +1495,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\OrderVisitorItemExtension\\:\\:__construct\\(\\) has parameter \\$nonFilteredCartAllowedOperations with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderVisitorItemExtension.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\OrderVisitorItemExtension\\:\\:applyToItem\\(\\) has no return type specified\\.$#" count: 1 @@ -1171,14 +1516,34 @@ parameters: path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderVisitorItemExtension.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Filter\\\\Doctrine\\\\CatalogPromotionChannelFilter\\:\\:__construct\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\TaxonItemExtension\\:\\:applyToItem\\(\\) has no return type specified\\.$#" count: 1 - path: src/Sylius/Bundle/ApiBundle/Filter/Doctrine/CatalogPromotionChannelFilter.php + path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/TaxonItemExtension.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Filter\\\\Doctrine\\\\CatalogPromotionChannelFilter\\:\\:getDescription\\(\\) return type has no value type specified in iterable type array\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\TaxonItemExtension\\:\\:applyToItem\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 - path: src/Sylius/Bundle/ApiBundle/Filter/Doctrine/CatalogPromotionChannelFilter.php + path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/TaxonItemExtension.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Doctrine\\\\QueryItemExtension\\\\TaxonItemExtension\\:\\:applyToItem\\(\\) has parameter \\$identifiers with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/TaxonItemExtension.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\EventSubscriber\\\\TaxonDeletionEventSubscriber\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/EventSubscriber/TaxonDeletionEventSubscriber.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Filter\\\\Doctrine\\\\ChannelsAwareChannelFilter\\:\\:__construct\\(\\) has parameter \\$properties with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ChannelsAwareChannelFilter.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Filter\\\\Doctrine\\\\ChannelsAwareChannelFilter\\:\\:getDescription\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ChannelsAwareChannelFilter.php - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Filter\\\\Doctrine\\\\ExchangeRateFilter\\:\\:filterProperty\\(\\) has no return type specified\\.$#" @@ -1320,21 +1685,26 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/Provider/ProductImageFilterProviderInterface.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ChannelPriceHistoryConfigDenormalizer\\:\\:__construct\\(\\) has parameter \\$channelPriceHistoryConfigFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Serializer/ChannelPriceHistoryConfigDenormalizer.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ChannelPriceHistoryConfigDenormalizer\\:\\:__construct\\(\\) has parameter \\$validationGroups with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Serializer/ChannelPriceHistoryConfigDenormalizer.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ChannelPriceHistoryConfigDenormalizer\\:\\:validateData\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Serializer/ChannelPriceHistoryConfigDenormalizer.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\CommandArgumentsDenormalizer\\:\\:getInputClassName\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Serializer/CommandArgumentsDenormalizer.php - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\CommandDenormalizer\\:\\:assertConstructorArgumentsPresence\\(\\) has parameter \\$data with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Serializer/CommandDenormalizer.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ProductAttributeValueNormalizer\\:\\:normalizeSelectValue\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Serializer/ProductAttributeValueNormalizer.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ProductAttributeValueNormalizer\\:\\:normalizeSelectValue\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -1355,21 +1725,31 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/Serializer/ProductNormalizer.php - - - message: "#^Parameter \\#1 \\$func of method Doctrine\\\\Common\\\\Collections\\\\ReadableCollection\\<\\(int\\|string\\),Sylius\\\\Component\\\\Product\\\\Model\\\\ProductVariantInterface\\>\\:\\:map\\(\\) expects Closure\\(Sylius\\\\Component\\\\Product\\\\Model\\\\ProductVariantInterface\\)\\: string, Closure\\(Sylius\\\\Component\\\\Core\\\\Model\\\\ProductVariantInterface\\)\\: string given\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Serializer/ProductNormalizer.php - - message: "#^PHPDoc tag @var for variable \\$appliedPromotions contains generic class Doctrine\\\\Common\\\\Collections\\\\ArrayCollection but does not specify its types\\: TKey, T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Serializer/ProductVariantNormalizer.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ShippingMethodNormalizer\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Serializer/ShippingMethodNormalizer.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ShippingMethodNormalizer\\:\\:__construct\\(\\) has parameter \\$shipmentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Serializer/ShippingMethodNormalizer.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ShippingMethodNormalizer\\:\\:isShopGetCollectionOperation\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Serializer/ShippingMethodNormalizer.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\TranslatableDenormalizer\\:\\:hasDefaultTranslation\\(\\) has parameter \\$translations with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Serializer/TranslatableDenormalizer.php + - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Serializer\\\\ZoneDenormalizer\\:\\:getZoneMemberByCode\\(\\) has parameter \\$zoneMembers with generic interface Doctrine\\\\Common\\\\Collections\\\\Selectable but does not specify its types\\: TKey, T$#" count: 1 @@ -1410,16 +1790,6 @@ parameters: count: 1 path: src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/LocaleContextBuilder.php - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Swagger\\\\AcceptLanguageHeaderDocumentationNormalizer\\:\\:__construct\\(\\) has parameter \\$localeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Swagger/AcceptLanguageHeaderDocumentationNormalizer.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Swagger\\\\PathHiderDocumentationNormalizer\\:\\:__construct\\(\\) has parameter \\$apiRoutes with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Swagger/PathHiderDocumentationNormalizer.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\CatalogPromotion\\\\FixedDiscountActionValidator\\:\\:isChannelAmountValid\\(\\) has parameter \\$channelConfiguration with no value type specified in iterable type array\\.$#" count: 1 @@ -1456,42 +1826,42 @@ parameters: path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/AccountVerificationTokenEligibilityValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\AccountVerificationTokenEligibilityValidator\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/AccountVerificationTokenEligibilityValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\AddingEligibleProductVariantToCartValidator\\:\\:validate\\(\\) has no return type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\AddingEligibleProductVariantToCartValidator\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/AddingEligibleProductVariantToCartValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\AddingEligibleProductVariantToCartValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\AddingEligibleProductVariantToCartValidator\\:\\:__construct\\(\\) has parameter \\$productVariantRepository with generic interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductVariantRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/AddingEligibleProductVariantToCartValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ChangedItemQuantityInCartValidator\\:\\:__construct\\(\\) has parameter \\$orderItemRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\AdminResetPasswordTokenNonExpiredValidator\\:\\:__construct\\(\\) has parameter \\$adminUserRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/AdminResetPasswordTokenNonExpiredValidator.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ChangedItemQuantityInCartValidator\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCartValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ChangedItemQuantityInCartValidator\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCartValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ChangedItemQuantityInCartValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCartValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ChosenPaymentMethodEligibilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ChosenPaymentMethodEligibilityValidator\\:\\:__construct\\(\\) has parameter \\$paymentMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentMethodRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenPaymentMethodEligibilityValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ChosenShippingMethodEligibilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ChosenPaymentMethodEligibilityValidator\\:\\:__construct\\(\\) has parameter \\$paymentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenPaymentMethodEligibilityValidator.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ChosenShippingMethodEligibilityValidator\\:\\:__construct\\(\\) has parameter \\$shipmentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenShippingMethodEligibilityValidator.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ChosenShippingMethodEligibilityValidator\\:\\:__construct\\(\\) has parameter \\$shippingMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShippingMethodRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenShippingMethodEligibilityValidator.php @@ -1501,75 +1871,85 @@ parameters: path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectOrderAddressValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\CorrectOrderAddressValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectOrderAddressValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderItemAvailabilityValidator\\:\\:validate\\(\\) has no return type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderItemAvailabilityValidator\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderItemAvailabilityValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderItemAvailabilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderItemAvailabilityValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderNotEmptyValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderNotEmptyValidator\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderNotEmptyValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderPaymentMethodEligibilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderPaymentMethodEligibilityValidator\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderPaymentMethodEligibilityValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderProductEligibilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderProductEligibilityValidator\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderProductEligibilityValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderShippingMethodEligibilityValidator\\:\\:validate\\(\\) has no return type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderShippingMethodEligibilityValidator\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderShippingMethodEligibilityValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\OrderShippingMethodEligibilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderShippingMethodEligibilityValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\PromotionCouponEligibilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\PromotionCouponEligibilityValidator\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/PromotionCouponEligibilityValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ShipmentAlreadyShippedValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\PromotionCouponEligibilityValidator\\:\\:__construct\\(\\) has parameter \\$promotionCouponRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\PromotionCouponRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/PromotionCouponEligibilityValidator.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ShipmentAlreadyShippedValidator\\:\\:__construct\\(\\) has parameter \\$shipmentRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShipmentAlreadyShippedValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ShopUserNotVerifiedValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\ShopUserNotVerifiedValidator\\:\\:__construct\\(\\) has parameter \\$shopUserRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserNotVerifiedValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\UniqueReviewerEmailValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\UniqueReviewerEmailValidator\\:\\:__construct\\(\\) has parameter \\$shopUserRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueReviewerEmailValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\UniqueShopUserEmailValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\UniqueShopUserEmailValidator\\:\\:__construct\\(\\) has parameter \\$shopUserRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueShopUserEmailValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\UpdateCartEmailNotAllowedValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\Constraints\\\\UpdateCartEmailNotAllowedValidator\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ApiBundle/Validator/Constraints/UpdateCartEmailNotAllowedValidator.php + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\ResourceApiInputDataPropertiesValidator\\:\\:validate\\(\\) has parameter \\$inputData with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Validator/ResourceApiInputDataPropertiesValidator.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\ResourceApiInputDataPropertiesValidator\\:\\:validate\\(\\) has parameter \\$validationGroups with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Validator/ResourceApiInputDataPropertiesValidator.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\ResourceInputDataPropertiesValidatorInterface\\:\\:validate\\(\\) has parameter \\$inputData with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Validator/ResourceInputDataPropertiesValidatorInterface.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ApiBundle\\\\Validator\\\\ResourceInputDataPropertiesValidatorInterface\\:\\:validate\\(\\) has parameter \\$validationGroups with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/ApiBundle/Validator/ResourceInputDataPropertiesValidatorInterface.php + - message: "#^Method Sylius\\\\Bundle\\\\AttributeBundle\\\\DependencyInjection\\\\SyliusAttributeExtension\\:\\:resolveResources\\(\\) has parameter \\$resources with no value type specified in iterable type array\\.$#" count: 1 @@ -1585,26 +1965,11 @@ parameters: count: 1 path: src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/Subscriber/LoadMetadataSubscriber.php - - - message: "#^Method Sylius\\\\Bundle\\\\AttributeBundle\\\\Doctrine\\\\ORM\\\\Subscriber\\\\LoadMetadataSubscriber\\:\\:mapManyToOne\\(\\) has parameter \\$subjectMapping with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/Subscriber/LoadMetadataSubscriber.php - - message: "#^Method Sylius\\\\Bundle\\\\AttributeBundle\\\\Form\\\\Type\\\\AttributeChoiceType\\:\\:__construct\\(\\) has parameter \\$attributeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeChoiceType.php - - - message: "#^Method Sylius\\\\Bundle\\\\AttributeBundle\\\\Form\\\\Type\\\\AttributeType\\\\Configuration\\\\SelectAttributeChoicesCollectionType\\:\\:resolveValues\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeType/Configuration/SelectAttributeChoicesCollectionType.php - - - - message: "#^Method Sylius\\\\Bundle\\\\AttributeBundle\\\\Form\\\\Type\\\\AttributeType\\\\Configuration\\\\SelectAttributeChoicesCollectionType\\:\\:resolveValues\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeType/Configuration/SelectAttributeChoicesCollectionType.php - - message: "#^Method Sylius\\\\Bundle\\\\AttributeBundle\\\\Form\\\\Type\\\\AttributeTypeChoiceType\\:\\:__construct\\(\\) has parameter \\$attributeTypes with no value type specified in iterable type array\\.$#" count: 1 @@ -1626,19 +1991,9 @@ parameters: path: src/Sylius/Bundle/AttributeBundle/SyliusAttributeBundle.php - - message: "#^Method Sylius\\\\Bundle\\\\AttributeBundle\\\\Validator\\\\Constraints\\\\ValidAttributeValueValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ChannelBundle\\\\Collector\\\\ChannelCollector\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidAttributeValueValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\AttributeBundle\\\\Validator\\\\Constraints\\\\ValidSelectAttributeConfigurationValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidSelectAttributeConfigurationValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\AttributeBundle\\\\Validator\\\\Constraints\\\\ValidTextAttributeConfigurationValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidTextAttributeConfigurationValidator.php + path: src/Sylius/Bundle/ChannelBundle/Collector/ChannelCollector.php - message: "#^Method Sylius\\\\Bundle\\\\ChannelBundle\\\\Collector\\\\ChannelCollector\\:\\:getChannel\\(\\) return type has no value type specified in iterable type array\\.$#" @@ -1650,6 +2005,16 @@ parameters: count: 1 path: src/Sylius/Bundle/ChannelBundle/Collector/ChannelCollector.php + - + message: "#^Method Sylius\\\\Bundle\\\\ChannelBundle\\\\Context\\\\FakeChannel\\\\FakeChannelContext\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ChannelBundle/Context/FakeChannel/FakeChannelContext.php + + - + message: "#^Method Sylius\\\\Bundle\\\\ChannelBundle\\\\Doctrine\\\\ORM\\\\ChannelRepository\\:\\:findAllWithBasicData\\(\\) return type has no value type specified in iterable type iterable\\.$#" + count: 1 + path: src/Sylius/Bundle/ChannelBundle/Doctrine/ORM/ChannelRepository.php + - message: "#^Method Sylius\\\\Bundle\\\\ChannelBundle\\\\Form\\\\Type\\\\ChannelChoiceType\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -1670,11 +2035,6 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Applicator/ActionBasedDiscountApplicator.php - - - message: "#^Parameter \\#1 \\$channel of method Sylius\\\\Component\\\\Core\\\\Model\\\\ProductVariantInterface\\:\\:getChannelPricingForChannel\\(\\) expects Sylius\\\\Component\\\\Core\\\\Model\\\\ChannelInterface, Sylius\\\\Component\\\\Channel\\\\Model\\\\ChannelInterface given\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Applicator/CatalogPromotionApplicator.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Calculator\\\\CatalogPromotionPriceCalculator\\:\\:__construct\\(\\) has parameter \\$priceCalculators with no value type specified in iterable type iterable\\.$#" count: 1 @@ -1685,6 +2045,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Checker/CatalogPromotionEligibilityChecker.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Checker\\\\InForTaxonsScopeVariantChecker\\:\\:__construct\\(\\) has parameter \\$taxonRepository with generic interface Sylius\\\\Component\\\\Taxonomy\\\\Repository\\\\TaxonRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Checker/InForTaxonsScopeVariantChecker.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Checker\\\\ProductVariantForCatalogPromotionEligibility\\:\\:__construct\\(\\) has parameter \\$locator with generic class Symfony\\\\Component\\\\DependencyInjection\\\\ServiceLocator but does not specify its types\\: T$#" count: 1 @@ -1705,6 +2070,26 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandDispatcher/BatchedApplyCatalogPromotionsOnVariantsCommandDispatcher.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\CommandHandler\\\\ApplyCatalogPromotionsOnVariantsHandler\\:\\:__construct\\(\\) has parameter \\$productVariantRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductVariantRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/ApplyCatalogPromotionsOnVariantsHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\CommandHandler\\\\DisableCatalogPromotionHandler\\:\\:__construct\\(\\) has parameter \\$catalogPromotionRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\CatalogPromotionRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/DisableCatalogPromotionHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\CommandHandler\\\\RemoveCatalogPromotionHandler\\:\\:__construct\\(\\) has parameter \\$catalogPromotionRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\CatalogPromotionRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/RemoveCatalogPromotionHandler.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\CommandHandler\\\\RemoveInactiveCatalogPromotionHandler\\:\\:__construct\\(\\) has parameter \\$catalogPromotionRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\CatalogPromotionRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/RemoveInactiveCatalogPromotionHandler.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\CommandHandler\\\\UpdateCatalogPromotionStateHandler\\:\\:__construct\\(\\) has parameter \\$catalogPromotionRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -1736,9 +2121,39 @@ parameters: path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Listener/CatalogPromotionUpdatedListener.php - - message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(Sylius\\\\Component\\\\Product\\\\Model\\\\ProductVariantInterface\\)\\: mixed\\)\\|null, Closure\\(Sylius\\\\Component\\\\Core\\\\Model\\\\ProductVariantInterface\\)\\: string given\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Listener\\\\ProductCreatedListener\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/ProductCatalogPromotionsProcessor.php + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Listener/ProductCreatedListener.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Listener\\\\ProductUpdatedListener\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Listener/ProductUpdatedListener.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Listener\\\\ProductVariantCreatedListener\\:\\:__construct\\(\\) has parameter \\$productVariantRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductVariantRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Listener/ProductVariantCreatedListener.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Listener\\\\ProductVariantUpdatedListener\\:\\:__construct\\(\\) has parameter \\$productVariantRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductVariantRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Listener/ProductVariantUpdatedListener.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Processor\\\\AllProductVariantsCatalogPromotionsProcessor\\:\\:__construct\\(\\) has parameter \\$productVariantRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductVariantRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/AllProductVariantsCatalogPromotionsProcessor.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Processor\\\\CatalogPromotionRemovalProcessor\\:\\:__construct\\(\\) has parameter \\$catalogPromotionRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\CatalogPromotionRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/CatalogPromotionRemovalProcessor.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Validator\\\\CatalogPromotionAction\\\\FixedDiscountActionValidator\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Validator/CatalogPromotionAction/FixedDiscountActionValidator.php - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Validator\\\\CatalogPromotionAction\\\\FixedDiscountActionValidator\\:\\:isChannelConfigured\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" @@ -1750,16 +2165,31 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Validator/CatalogPromotionAction/FixedDiscountActionValidator.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Validator\\\\CatalogPromotionScope\\\\ForProductsScopeValidator\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Validator/CatalogPromotionScope/ForProductsScopeValidator.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Validator\\\\CatalogPromotionScope\\\\ForProductsScopeValidator\\:\\:validate\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Validator/CatalogPromotionScope/ForProductsScopeValidator.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Validator\\\\CatalogPromotionScope\\\\ForTaxonsScopeValidator\\:\\:__construct\\(\\) has parameter \\$taxonRepository with generic interface Sylius\\\\Component\\\\Taxonomy\\\\Repository\\\\TaxonRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Validator/CatalogPromotionScope/ForTaxonsScopeValidator.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Validator\\\\CatalogPromotionScope\\\\ForTaxonsScopeValidator\\:\\:validate\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Validator/CatalogPromotionScope/ForTaxonsScopeValidator.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Validator\\\\CatalogPromotionScope\\\\ForVariantsScopeValidator\\:\\:__construct\\(\\) has parameter \\$variantRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductVariantRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/CatalogPromotion/Validator/CatalogPromotionScope/ForVariantsScopeValidator.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\CatalogPromotion\\\\Validator\\\\CatalogPromotionScope\\\\ForVariantsScopeValidator\\:\\:validate\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -1826,39 +2256,9 @@ parameters: path: src/Sylius/Bundle/CoreBundle/Collector/SyliusCollector.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Command\\\\AbstractInstallCommand\\:\\:initialize\\(\\) has no return type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Context\\\\CustomerAndChannelBasedCartContext\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Command\\\\AbstractInstallCommand\\:\\:renderTable\\(\\) has parameter \\$headers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Command\\\\AbstractInstallCommand\\:\\:renderTable\\(\\) has parameter \\$rows with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Command\\\\AbstractInstallCommand\\:\\:runCommands\\(\\) has parameter \\$commands with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php - - - - message: "#^PHPDoc tag @var for variable \\$command has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Command/InstallCommand.php - - - - message: "#^Property Sylius\\\\Bundle\\\\CoreBundle\\\\Command\\\\InstallCommand\\:\\:\\$commands type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Command/InstallCommand.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Controller\\\\ProductTaxonController\\:\\:shouldProductsPositionsBeUpdated\\(\\) has parameter \\$productTaxons with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Controller/ProductTaxonController.php + path: src/Sylius/Bundle/CoreBundle/Context/CustomerAndChannelBasedCartContext.php - message: "#^PHPDoc tag @var for variable \\$smConfigs has no value type specified in iterable type array\\.$#" @@ -1870,6 +2270,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/DependencyInjection/SyliusCoreExtension.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\DependencyInjection\\\\SyliusCoreExtension\\:\\:prependSyliusOrderBundle\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/DependencyInjection/SyliusCoreExtension.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\DependencyInjection\\\\SyliusCoreExtension\\:\\:switchOrderProcessorsPriorities\\(\\) has no return type specified\\.$#" count: 1 @@ -2045,16 +2450,16 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/EventListener/DefaultUsernameORMListener.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\EventListener\\\\TaxonDeletionListener\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/EventListener/TaxonDeletionListener.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Factory\\\\OrderFactory\\:\\:__construct\\(\\) has parameter \\$decoratedFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Factory/OrderFactory.php - - - message: "#^Interface Sylius\\\\Bundle\\\\CoreBundle\\\\Factory\\\\OrderFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Factory/OrderFactoryInterface.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\AbstractResourceFixture\\:\\:load\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 @@ -2100,11 +2505,6 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/AddressExampleFactory.php - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\AddressExampleFactory\\:\\:getProvinceCode\\(\\) has parameter \\$provinces with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection but does not specify its types\\: TKey, T$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/AddressExampleFactory.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\AddressExampleFactory\\:\\:provideProvince\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 @@ -2150,6 +2550,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/CatalogPromotionExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\CatalogPromotionExampleFactory\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/CatalogPromotionExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\CatalogPromotionExampleFactory\\:\\:__construct\\(\\) has parameter \\$localeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -2175,6 +2580,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/CatalogPromotionScopeExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ChannelExampleFactory\\:\\:__construct\\(\\) has parameter \\$channelFactory with generic interface Sylius\\\\Component\\\\Channel\\\\Factory\\\\ChannelFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ChannelExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ChannelExampleFactory\\:\\:__construct\\(\\) has parameter \\$currencyRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -2190,6 +2600,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ChannelExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ChannelExampleFactory\\:\\:__construct\\(\\) has parameter \\$taxonRepository with generic interface Sylius\\\\Component\\\\Taxonomy\\\\Repository\\\\TaxonRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ChannelExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ChannelExampleFactory\\:\\:__construct\\(\\) has parameter \\$zoneRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -2205,6 +2620,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ChannelExampleFactory.php + - + message: "#^Property Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ChannelExampleFactory\\:\\:\\$taxonRepository with generic interface Sylius\\\\Component\\\\Taxonomy\\\\Repository\\\\TaxonRepositoryInterface does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ChannelExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\CustomerGroupExampleFactory\\:\\:__construct\\(\\) has parameter \\$customerGroupFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -2250,16 +2670,41 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/OrderExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\OrderExampleFactory\\:\\:__construct\\(\\) has parameter \\$paymentMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/OrderExampleFactory.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\OrderExampleFactory\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/OrderExampleFactory.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\OrderExampleFactory\\:\\:__construct\\(\\) has parameter \\$shippingMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShippingMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/OrderExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\OrderExampleFactory\\:\\:create\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/OrderExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\PaymentMethodExampleFactory\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/PaymentMethodExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\PaymentMethodExampleFactory\\:\\:__construct\\(\\) has parameter \\$localeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/PaymentMethodExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\PaymentMethodExampleFactory\\:\\:__construct\\(\\) has parameter \\$paymentMethodFactory with generic interface Sylius\\\\Component\\\\Core\\\\Factory\\\\PaymentMethodFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/PaymentMethodExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\PaymentMethodExampleFactory\\:\\:create\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 @@ -2275,6 +2720,16 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductAssociationExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ProductAssociationExampleFactory\\:\\:__construct\\(\\) has parameter \\$productAssociationTypeRepository with generic interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductAssociationTypeRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductAssociationExampleFactory.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ProductAssociationExampleFactory\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductAssociationExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ProductAssociationExampleFactory\\:\\:create\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 @@ -2310,6 +2765,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductAttributeExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ProductAttributeExampleFactory\\:\\:__construct\\(\\) has parameter \\$productAttributeFactory with generic interface Sylius\\\\Component\\\\Attribute\\\\Factory\\\\AttributeFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductAttributeExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ProductAttributeExampleFactory\\:\\:create\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 @@ -2460,16 +2920,41 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductOptionExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ProductReviewExampleFactory\\:\\:__construct\\(\\) has parameter \\$customerRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\CustomerRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductReviewExampleFactory.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ProductReviewExampleFactory\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductReviewExampleFactory.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ProductReviewExampleFactory\\:\\:__construct\\(\\) has parameter \\$productReviewFactory with generic interface Sylius\\\\Component\\\\Review\\\\Factory\\\\ReviewFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductReviewExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ProductReviewExampleFactory\\:\\:create\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductReviewExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\PromotionActionExampleFactory\\:\\:__construct\\(\\) has parameter \\$promotionActionFactory with generic interface Sylius\\\\Component\\\\Core\\\\Factory\\\\PromotionActionFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionActionExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\PromotionActionExampleFactory\\:\\:create\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionActionExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\PromotionExampleFactory\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\PromotionExampleFactory\\:\\:__construct\\(\\) has parameter \\$couponFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -2490,6 +2975,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\PromotionRuleExampleFactory\\:\\:__construct\\(\\) has parameter \\$promotionRuleFactory with generic interface Sylius\\\\Component\\\\Core\\\\Factory\\\\PromotionRuleFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionRuleExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\PromotionRuleExampleFactory\\:\\:create\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 @@ -2505,6 +2995,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ShippingCategoryExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ShippingMethodExampleFactory\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/ShippingMethodExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\ShippingMethodExampleFactory\\:\\:__construct\\(\\) has parameter \\$localeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -2600,6 +3095,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/TaxonExampleFactory.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\TaxonExampleFactory\\:\\:__construct\\(\\) has parameter \\$taxonRepository with generic interface Sylius\\\\Component\\\\Taxonomy\\\\Repository\\\\TaxonRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Factory/TaxonExampleFactory.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Factory\\\\TaxonExampleFactory\\:\\:create\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 @@ -2630,6 +3130,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/GeographicalFixture.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\GeographicalFixture\\:\\:__construct\\(\\) has parameter \\$zoneFactory with generic interface Sylius\\\\Component\\\\Addressing\\\\Factory\\\\ZoneFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/GeographicalFixture.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\GeographicalFixture\\:\\:getZoneMembers\\(\\) has parameter \\$zoneOptions with no value type specified in iterable type array\\.$#" count: 1 @@ -2675,6 +3180,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/GeographicalFixture.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Listener\\\\CatalogPromotionExecutorListener\\:\\:__construct\\(\\) has parameter \\$catalogPromotionsRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\CatalogPromotionRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/Listener/CatalogPromotionExecutorListener.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\Listener\\\\CatalogPromotionExecutorListener\\:\\:__construct\\(\\) has parameter \\$defaultCriteria with no value type specified in iterable type iterable\\.$#" count: 1 @@ -2820,11 +3330,21 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/OrderFixture.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\OrderFixture\\:\\:__construct\\(\\) has parameter \\$paymentMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/OrderFixture.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\OrderFixture\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/OrderFixture.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\OrderFixture\\:\\:__construct\\(\\) has parameter \\$shippingMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShippingMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/OrderFixture.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\OrderFixture\\:\\:generateDates\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -2845,6 +3365,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Fixture/ProductAttributeFixture.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\SimilarProductAssociationFixture\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Fixture/SimilarProductAssociationFixture.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Fixture\\\\SimilarProductAssociationFixture\\:\\:load\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 @@ -2895,6 +3420,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Form/DataTransformer/ProductVariantsToCodesTransformer.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\DataTransformer\\\\ProductVariantsToCodesTransformer\\:\\:__construct\\(\\) has parameter \\$productVariantRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductVariantRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Form/DataTransformer/ProductVariantsToCodesTransformer.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\DataTransformer\\\\ProductVariantsToCodesTransformer\\:\\:reverseTransform\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -2910,6 +3440,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Form/DataTransformer/ProductsToCodesTransformer.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\DataTransformer\\\\ProductsToCodesTransformer\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Form/DataTransformer/ProductsToCodesTransformer.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\DataTransformer\\\\ProductsToCodesTransformer\\:\\:reverseTransform\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -2925,6 +3460,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Form/DataTransformer/TaxonsToCodesTransformer.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\DataTransformer\\\\TaxonsToCodesTransformer\\:\\:__construct\\(\\) has parameter \\$taxonRepository with generic interface Sylius\\\\Component\\\\Taxonomy\\\\Repository\\\\TaxonRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Form/DataTransformer/TaxonsToCodesTransformer.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\DataTransformer\\\\TaxonsToCodesTransformer\\:\\:reverseTransform\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -2955,6 +3495,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Form/Extension/LocaleTypeExtension.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\Type\\\\AddressChoiceType\\:\\:__construct\\(\\) has parameter \\$addressRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\AddressRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Form/Type/AddressChoiceType.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\Type\\\\CatalogPromotionScope\\\\ForProductsScopeConfigurationType\\:\\:__construct\\(\\) has parameter \\$productsToCodesTransformer with generic interface Symfony\\\\Component\\\\Form\\\\DataTransformerInterface but does not specify its types\\: T, R$#" count: 1 @@ -2970,6 +3515,21 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForVariantsScopeConfigurationType.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\Type\\\\ChannelCollectionType\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Form/Type/ChannelCollectionType.php + + - + message: "#^PHPDoc tag @var for variable \\$excludedTaxons contains generic interface Doctrine\\\\Common\\\\Collections\\\\Collection but does not specify its types\\: TKey, T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Form/Type/ChannelPriceHistoryConfigType.php + + - + message: "#^PHPDoc tag @var for variable \\$traversableForms has no value type specified in iterable type Traversable\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Form/Type/ChannelPriceHistoryConfigType.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Form\\\\Type\\\\Customer\\\\CustomerCheckoutGuestType\\:\\:__construct\\(\\) has parameter \\$customerFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -3075,31 +3635,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Installer/Executor/CommandExecutor.php - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Installer\\\\Provider\\\\DatabaseSetupCommandsProvider\\:\\:dropSchemaAndGetMigrateOrSchemaCreateCommands\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Installer/Provider/DatabaseSetupCommandsProvider.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Installer\\\\Provider\\\\DatabaseSetupCommandsProvider\\:\\:getCommands\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Installer/Provider/DatabaseSetupCommandsProvider.php - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Installer\\\\Provider\\\\DatabaseSetupCommandsProvider\\:\\:getCreateDatabaseWithSchemaCommands\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Installer/Provider/DatabaseSetupCommandsProvider.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Installer\\\\Provider\\\\DatabaseSetupCommandsProvider\\:\\:getCreateSchemaOrRunMigrationsCommand\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Installer/Provider/DatabaseSetupCommandsProvider.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Installer\\\\Provider\\\\DatabaseSetupCommandsProvider\\:\\:getSchemaManager\\(\\) return type with generic class Doctrine\\\\DBAL\\\\Schema\\\\AbstractSchemaManager does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Installer/Provider/DatabaseSetupCommandsProvider.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Installer\\\\Provider\\\\DatabaseSetupCommandsProviderInterface\\:\\:getCommands\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -3125,21 +3665,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Installer/Renderer/TableRenderer.php - - - message: "#^Class Sylius\\\\Bundle\\\\CoreBundle\\\\Installer\\\\Requirement\\\\RequirementCollection implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Installer/Requirement/RequirementCollection.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Installer\\\\Requirement\\\\RequirementCollection\\:\\:getIterator\\(\\) return type with generic class ArrayIterator does not specify its types\\: TKey, TValue$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Installer/Requirement/RequirementCollection.php - - - message: "#^Class Sylius\\\\Bundle\\\\CoreBundle\\\\Installer\\\\Requirement\\\\SyliusRequirements implements generic interface IteratorAggregate but does not specify its types\\: TKey, TValue$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Installer/Requirement/SyliusRequirements.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Installer\\\\Requirement\\\\SyliusRequirements\\:\\:getIterator\\(\\) return type with generic class ArrayIterator does not specify its types\\: TKey, TValue$#" count: 1 @@ -3175,11 +3705,21 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Installer/Setup/LocaleSetup.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\MessageHandler\\\\Admin\\\\Account\\\\RequestResetPasswordEmailHandler\\:\\:__construct\\(\\) has parameter \\$userRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/MessageHandler/Admin/Account/RequestResetPasswordEmailHandler.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\MessageHandler\\\\Admin\\\\Account\\\\RequestResetPasswordEmailHandler\\:\\:__invoke\\(\\) has no return type specified\\.$#" count: 1 path: src/Sylius/Bundle/CoreBundle/MessageHandler/Admin/Account/RequestResetPasswordEmailHandler.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\MessageHandler\\\\Admin\\\\Account\\\\SendResetPasswordEmailHandler\\:\\:__construct\\(\\) has parameter \\$userRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/MessageHandler/Admin/Account/SendResetPasswordEmailHandler.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Migrations\\\\Version20161214153137\\:\\:setContainer\\(\\) has no return type specified\\.$#" count: 1 @@ -3235,6 +3775,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/OAuth/UserProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\OAuth\\\\UserProvider\\:\\:__construct\\(\\) has parameter \\$customerRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\CustomerRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/OAuth/UserProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\OAuth\\\\UserProvider\\:\\:__construct\\(\\) has parameter \\$oauthFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -3251,7 +3796,12 @@ parameters: path: src/Sylius/Bundle/CoreBundle/OAuth/UserProvider.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Order\\\\Checker\\\\OrderPromotionsIntegrityChecker\\:\\:check\\(\\) should return Sylius\\\\Component\\\\Core\\\\Model\\\\PromotionInterface\\|null but returns Sylius\\\\Component\\\\Promotion\\\\Model\\\\PromotionInterface\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\OAuth\\\\UserProvider\\:\\:__construct\\(\\) has parameter \\$userRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/OAuth/UserProvider.php + + - + message: "#^PHPDoc tag @var for variable \\$previousPromotions contains generic class Doctrine\\\\Common\\\\Collections\\\\ArrayCollection but does not specify its types\\: TKey, T$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Order/Checker/OrderPromotionsIntegrityChecker.php @@ -3265,6 +3815,91 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Order/NumberGenerator/SequentialOrderNumberGenerator.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\Command\\\\ApplyLowestPriceOnChannelPricings\\:\\:__construct\\(\\) has parameter \\$channelPricingIds with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/Command/ApplyLowestPriceOnChannelPricings.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\CommandDispatcher\\\\BatchedApplyLowestPriceOnChannelPricingsCommandDispatcher\\:\\:__construct\\(\\) has parameter \\$channelPricingRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/CommandDispatcher/BatchedApplyLowestPriceOnChannelPricingsCommandDispatcher.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\CommandDispatcher\\\\BatchedApplyLowestPriceOnChannelPricingsCommandDispatcher\\:\\:getIdsBatch\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/CommandDispatcher/BatchedApplyLowestPriceOnChannelPricingsCommandDispatcher.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\CommandHandler\\\\ApplyLowestPriceOnChannelPricingsHandler\\:\\:__construct\\(\\) has parameter \\$channelPricingRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/CommandHandler/ApplyLowestPriceOnChannelPricingsHandler.php + + - + message: "#^Property Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\EntityObserver\\\\ProcessLowestPricesOnChannelChangeObserver\\:\\:\\$channelsCurrentlyProcessed type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelChangeObserver.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\EntityObserver\\\\ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserver\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserver.php + + - + message: "#^Property Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\EntityObserver\\\\ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserver\\:\\:\\$configsCurrentlyProcessed type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserver.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\EventListener\\\\ChannelPricingLogEntryEventListener\\:\\:postPersist\\(\\) has parameter \\$event with generic class Doctrine\\\\Persistence\\\\Event\\\\LifecycleEventArgs but does not specify its types\\: TObjectManager$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/EventListener/ChannelPricingLogEntryEventListener.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\EventListener\\\\OnFlushEntityObserverListener\\:\\:__construct\\(\\) has parameter \\$entityObservers with no value type specified in iterable type iterable\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/EventListener/OnFlushEntityObserverListener.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\EventListener\\\\OnFlushEntityObserverListener\\:\\:isEntityChanged\\(\\) has parameter \\$supportedFields with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/EventListener/OnFlushEntityObserverListener.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\Logger\\\\PriceChangeLogger\\:\\:__construct\\(\\) has parameter \\$logEntryFactory with generic interface Sylius\\\\Component\\\\Core\\\\Factory\\\\ChannelPricingLogEntryFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/Logger/PriceChangeLogger.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\Processor\\\\ProductLowestPriceBeforeDiscountProcessor\\:\\:__construct\\(\\) has parameter \\$channelPricingLogEntryRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ChannelPricingLogEntryRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/Processor/ProductLowestPriceBeforeDiscountProcessor.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\Processor\\\\ProductLowestPriceBeforeDiscountProcessor\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/Processor/ProductLowestPriceBeforeDiscountProcessor.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\Remover\\\\ChannelPricingLogEntriesRemover\\:\\:__construct\\(\\) has parameter \\$channelPricingLogEntriesRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ChannelPricingLogEntryRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/Remover/ChannelPricingLogEntriesRemover.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\Remover\\\\ChannelPricingLogEntriesRemover\\:\\:getBatch\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/Remover/ChannelPricingLogEntriesRemover.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\PriceHistory\\\\Remover\\\\ChannelPricingLogEntriesRemover\\:\\:processDeletion\\(\\) has parameter \\$deletedChannelPricingLogEntries with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/PriceHistory/Remover/ChannelPricingLogEntriesRemover.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Provider\\\\CustomerProvider\\:\\:__construct\\(\\) has parameter \\$customerRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\CustomerRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Provider/CustomerProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Resolver\\\\CustomerResolver\\:\\:__construct\\(\\) has parameter \\$customerFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -3275,11 +3910,31 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/SectionResolver/UriBasedSectionProvider.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Security\\\\UserPasswordResetter\\:\\:__construct\\(\\) has parameter \\$userRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Security/UserPasswordResetter.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Storage\\\\CartSessionStorage\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Storage/CartSessionStorage.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\SyliusCoreBundle\\:\\:getSupportedDrivers\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Bundle/CoreBundle/SyliusCoreBundle.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Taxation\\\\Strategy\\\\TaxCalculationStrategy\\:\\:__construct\\(\\) has parameter \\$applicators with no value type specified in iterable type iterable\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Taxation/Strategy/TaxCalculationStrategy.php + + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Templating\\\\Helper\\\\PriceHelper\\:\\:getLowestPriceBeforeDiscount\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Templating/Helper/PriceHelper.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Templating\\\\Helper\\\\PriceHelper\\:\\:getOriginalPrice\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -3295,6 +3950,11 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Templating/Helper/PriceHelper.php + - + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Templating\\\\Helper\\\\PriceHelper\\:\\:hasLowestPriceBeforeDiscount\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Templating/Helper/PriceHelper.php + - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Templating\\\\Helper\\\\ProductVariantsPricesHelper\\:\\:getPrices\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -3311,24 +3971,9 @@ parameters: path: src/Sylius/Bundle/CoreBundle/Twig/FilterExtension.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\CartItemAvailabilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\ExistingChannelCodeValidator\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/CartItemAvailabilityValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\ChannelDefaultLocaleEnabledValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelDefaultLocaleEnabledValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\HasAllPricesDefinedValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasAllPricesDefinedValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\HasAllVariantPricesDefinedValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasAllVariantPricesDefinedValidator.php + path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/ExistingChannelCodeValidator.php - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\HasEnabledEntityValidator\\:\\:isLastEnabledEntity\\(\\) has parameter \\$result with no value type specified in iterable type array\\.$#" @@ -3340,48 +3985,18 @@ parameters: count: 1 path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasEnabledEntityValidator.php - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\HasEnabledEntityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasEnabledEntityValidator.php - - - - message: "#^Unable to resolve the template type T in call to method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getRepository\\(\\)$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasEnabledEntityValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\LocalesAwareValidAttributeValueValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/LocalesAwareValidAttributeValueValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\OrderPaymentMethodEligibilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderPaymentMethodEligibilityValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\OrderProductEligibilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderProductEligibilityValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\OrderShippingMethodEligibilityValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderShippingMethodEligibilityValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\RegisteredUserValidator\\:\\:__construct\\(\\) has parameter \\$customerRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/RegisteredUserValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\RegisteredUserValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\TranslationForExistingLocalesValidator\\:\\:__construct\\(\\) has parameter \\$localeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/RegisteredUserValidator.php + path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/TranslationForExistingLocalesValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\UniqueReviewerEmailValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\CoreBundle\\\\Validator\\\\Constraints\\\\UniqueReviewerEmailValidator\\:\\:__construct\\(\\) has parameter \\$userRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/CoreBundle/Validator/Constraints/UniqueReviewerEmailValidator.php @@ -3401,24 +4016,14 @@ parameters: path: src/Sylius/Bundle/CurrencyBundle/SyliusCurrencyBundle.php - - message: "#^Method Sylius\\\\Bundle\\\\CurrencyBundle\\\\Validator\\\\Constraints\\\\DifferentSourceTargetCurrencyValidator\\:\\:validate\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/CurrencyBundle/Validator/Constraints/DifferentSourceTargetCurrencyValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CurrencyBundle\\\\Validator\\\\Constraints\\\\DifferentSourceTargetCurrencyValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/CurrencyBundle/Validator/Constraints/DifferentSourceTargetCurrencyValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\CurrencyBundle\\\\Validator\\\\Constraints\\\\UniqueCurrencyPairValidator\\:\\:validate\\(\\) has no return type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\CurrencyBundle\\\\Validator\\\\Constraints\\\\UniqueCurrencyPairValidator\\:\\:__construct\\(\\) has parameter \\$exchangeRateRepository with generic interface Sylius\\\\Component\\\\Currency\\\\Repository\\\\ExchangeRateRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/CurrencyBundle/Validator/Constraints/UniqueCurrencyPairValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\CurrencyBundle\\\\Validator\\\\Constraints\\\\UniqueCurrencyPairValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\CustomerBundle\\\\Doctrine\\\\ORM\\\\CustomerGroupRepository\\:\\:findByPhrase\\(\\) return type has no value type specified in iterable type iterable\\.$#" count: 1 - path: src/Sylius/Bundle/CurrencyBundle/Validator/Constraints/UniqueCurrencyPairValidator.php + path: src/Sylius/Bundle/CustomerBundle/Doctrine/ORM/CustomerGroupRepository.php - message: "#^Method Sylius\\\\Bundle\\\\CustomerBundle\\\\Form\\\\Type\\\\CustomerChoiceType\\:\\:__construct\\(\\) has parameter \\$customerRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" @@ -3446,9 +4051,9 @@ parameters: path: src/Sylius/Bundle/InventoryBundle/SyliusInventoryBundle.php - - message: "#^Method Sylius\\\\Bundle\\\\InventoryBundle\\\\Validator\\\\Constraints\\\\InStockValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\InventoryBundle\\\\Validator\\\\Constraints\\\\InStock\\:\\:getTargets\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 - path: src/Sylius/Bundle/InventoryBundle/Validator/Constraints/InStockValidator.php + path: src/Sylius/Bundle/InventoryBundle/Validator/Constraints/InStock.php - message: "#^Method Sylius\\\\Bundle\\\\LocaleBundle\\\\Form\\\\Type\\\\LocaleChoiceType\\:\\:__construct\\(\\) has parameter \\$localeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" @@ -3460,6 +4065,26 @@ parameters: count: 1 path: src/Sylius/Bundle/LocaleBundle/SyliusLocaleBundle.php + - + message: "#^Method Sylius\\\\Bundle\\\\OrderBundle\\\\Context\\\\SessionBasedCartContext\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/OrderBundle/Context/SessionBasedCartContext.php + + - + message: "#^Method Sylius\\\\Bundle\\\\OrderBundle\\\\Controller\\\\OrderController\\:\\:addFlash\\(\\) has parameter \\$message with no type specified\\.$#" + count: 1 + path: src/Sylius/Bundle/OrderBundle/Controller/OrderController.php + + - + message: "#^Method Sylius\\\\Bundle\\\\OrderBundle\\\\Controller\\\\OrderController\\:\\:getOrderRepository\\(\\) return type with generic interface Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderRepositoryInterface does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/OrderBundle/Controller/OrderController.php + + - + message: "#^Method Sylius\\\\Bundle\\\\OrderBundle\\\\Controller\\\\OrderItemController\\:\\:getOrderRepository\\(\\) return type with generic interface Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderRepositoryInterface does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/OrderBundle/Controller/OrderItemController.php + - message: "#^Method Sylius\\\\Bundle\\\\OrderBundle\\\\Doctrine\\\\ORM\\\\OrderItemRepository\\:\\:findOneByIdAndCartId\\(\\) has parameter \\$cartId with no type specified\\.$#" count: 1 @@ -3500,6 +4125,11 @@ parameters: count: 1 path: src/Sylius/Bundle/OrderBundle/NumberGenerator/SequentialOrderNumberGenerator.php + - + message: "#^Method Sylius\\\\Bundle\\\\OrderBundle\\\\Remover\\\\ExpiredCartsRemover\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/OrderBundle/Remover/ExpiredCartsRemover.php + - message: "#^Method Sylius\\\\Bundle\\\\OrderBundle\\\\Remover\\\\ExpiredCartsRemover\\:\\:getBatch\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -3545,6 +4175,11 @@ parameters: count: 1 path: src/Sylius/Bundle/PayumBundle/Action/Paypal/ExpressCheckout/ConvertPaymentAction.php + - + message: "#^Method Sylius\\\\Bundle\\\\PayumBundle\\\\Controller\\\\PayumController\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/PayumBundle/Controller/PayumController.php + - message: "#^Method Sylius\\\\Bundle\\\\PayumBundle\\\\Controller\\\\PayumController\\:\\:prepareCaptureAction\\(\\) has parameter \\$tokenValue with no type specified\\.$#" count: 1 @@ -3555,11 +4190,6 @@ parameters: count: 1 path: src/Sylius/Bundle/PayumBundle/Controller/PayumController.php - - - message: "#^Method Sylius\\\\Bundle\\\\PayumBundle\\\\DependencyInjection\\\\Compiler\\\\UnregisterStripeGatewayTypePass\\:\\:process\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterStripeGatewayTypePass.php - - message: "#^Method Sylius\\\\Bundle\\\\PayumBundle\\\\Factory\\\\GetStatusFactory\\:\\:createNewWithModel\\(\\) has parameter \\$model with no type specified\\.$#" count: 1 @@ -3665,6 +4295,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ProductBundle/EventListener/SelectProductAttributeChoiceRemoveListener.php + - + message: "#^PHPDoc tag @var for variable \\$productAttributeValueRepository contains generic interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductAttributeValueRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ProductBundle/EventListener/SelectProductAttributeChoiceRemoveListener.php + - message: "#^Unable to resolve the template type T in call to method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getRepository\\(\\)$#" count: 1 @@ -3696,7 +4331,7 @@ parameters: path: src/Sylius/Bundle/ProductBundle/Form/DataTransformer/ProductsToProductAssociationsTransformer.php - - message: "#^Method Sylius\\\\Bundle\\\\ProductBundle\\\\Form\\\\DataTransformer\\\\ProductsToProductAssociationsTransformer\\:\\:getCodesAsStringFromProducts\\(\\) has parameter \\$products with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection but does not specify its types\\: TKey, T$#" + message: "#^Method Sylius\\\\Bundle\\\\ProductBundle\\\\Form\\\\DataTransformer\\\\ProductsToProductAssociationsTransformer\\:\\:__construct\\(\\) has parameter \\$productRepository with generic interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ProductBundle/Form/DataTransformer/ProductsToProductAssociationsTransformer.php @@ -3715,11 +4350,6 @@ parameters: count: 1 path: src/Sylius/Bundle/ProductBundle/Form/EventSubscriber/BuildAttributesFormSubscriber.php - - - message: "#^Parameter \\#1 \\$p of method Doctrine\\\\Common\\\\Collections\\\\Collection\\<\\(int\\|string\\),Sylius\\\\Component\\\\Attribute\\\\Model\\\\AttributeValueInterface\\>\\:\\:filter\\(\\) expects Closure\\(Sylius\\\\Component\\\\Attribute\\\\Model\\\\AttributeValueInterface\\)\\: bool, Closure\\(Sylius\\\\Component\\\\Product\\\\Model\\\\ProductAttributeValueInterface\\)\\: bool given\\.$#" - count: 1 - path: src/Sylius/Bundle/ProductBundle/Form/EventSubscriber/BuildAttributesFormSubscriber.php - - message: "#^Method Sylius\\\\Bundle\\\\ProductBundle\\\\Form\\\\Type\\\\ProductAssociationTypeChoiceType\\:\\:__construct\\(\\) has parameter \\$productAssociationTypeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -3771,12 +4401,7 @@ parameters: path: src/Sylius/Bundle/ProductBundle/SyliusProductBundle.php - - message: "#^Method Sylius\\\\Bundle\\\\ProductBundle\\\\Validator\\\\ProductVariantCombinationValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/ProductBundle/Validator/ProductVariantCombinationValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\ProductBundle\\\\Validator\\\\UniqueSimpleProductCodeValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\ProductBundle\\\\Validator\\\\UniqueSimpleProductCodeValidator\\:\\:__construct\\(\\) has parameter \\$productVariantRepository with generic interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductVariantRepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Bundle/ProductBundle/Validator/UniqueSimpleProductCodeValidator.php @@ -3875,6 +4500,11 @@ parameters: count: 1 path: src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionRuleChoiceType.php + - + message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\Provider\\\\EligibleCatalogPromotionsProvider\\:\\:__construct\\(\\) has parameter \\$catalogPromotionRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\CatalogPromotionRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/PromotionBundle/Provider/EligibleCatalogPromotionsProvider.php + - message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\Provider\\\\EligibleCatalogPromotionsProvider\\:\\:__construct\\(\\) has parameter \\$defaultCriteria with no value type specified in iterable type iterable\\.$#" count: 1 @@ -3901,12 +4531,12 @@ parameters: path: src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionActionValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\CatalogPromotionActionValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Property Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\CatalogPromotionActionValidator\\:\\:\\$actionValidators type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionActionValidator.php - - message: "#^Property Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\CatalogPromotionActionValidator\\:\\:\\$actionValidators type has no value type specified in iterable type array\\.$#" + message: "#^Property Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\CatalogPromotionActionValidator\\:\\:\\$actionTypes is never read, only written.$#" count: 1 path: src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionActionValidator.php @@ -3921,12 +4551,12 @@ parameters: path: src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\CatalogPromotionScopeValidator\\:\\:__construct\\(\\) has parameter \\$scopeValidators with no value type specified in iterable type iterable\\.$#" + message: "#^Property Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\CatalogPromotionScopeValidator\\:\\:\\$scopeTypes is never read, only written.$#" count: 1 path: src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\CatalogPromotionScopeValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\CatalogPromotionScopeValidator\\:\\:__construct\\(\\) has parameter \\$scopeValidators with no value type specified in iterable type iterable\\.$#" count: 1 path: src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeValidator.php @@ -3935,21 +4565,6 @@ parameters: count: 1 path: src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeValidator.php - - - message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\CouponGenerationAmountValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/PromotionBundle/Validator/CouponGenerationAmountValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\PromotionDateRangeValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/PromotionBundle/Validator/PromotionDateRangeValidator.php - - - - message: "#^Method Sylius\\\\Bundle\\\\PromotionBundle\\\\Validator\\\\PromotionSubjectCouponValidator\\:\\:validate\\(\\) has parameter \\$value with no type specified\\.$#" - count: 1 - path: src/Sylius/Bundle/PromotionBundle/Validator/PromotionSubjectCouponValidator.php - - message: "#^Method Sylius\\\\Bundle\\\\ReviewBundle\\\\DependencyInjection\\\\SyliusReviewExtension\\:\\:createReviewListeners\\(\\) has parameter \\$reviewSubjects with no value type specified in iterable type array\\.$#" count: 1 @@ -4095,6 +4710,11 @@ parameters: count: 1 path: src/Sylius/Bundle/ShopBundle/EmailManager/ContactEmailManagerInterface.php + - + message: "#^Method Sylius\\\\Bundle\\\\ShopBundle\\\\EventListener\\\\UserImpersonatedListener\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/ShopBundle/EventListener/UserImpersonatedListener.php + - message: "#^Method Sylius\\\\Bundle\\\\ShopBundle\\\\Menu\\\\AccountMenuBuilder\\:\\:createMenu\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#" count: 1 @@ -4120,11 +4740,6 @@ parameters: count: 1 path: src/Sylius/Bundle/ShopBundle/SyliusShopBundle.php - - - message: "#^Parameter \\#1 \\$p of method Doctrine\\\\Common\\\\Collections\\\\Collection\\<\\(int\\|string\\),Sylius\\\\Component\\\\Payment\\\\Model\\\\PaymentInterface\\>\\:\\:filter\\(\\) expects Closure\\(Sylius\\\\Component\\\\Payment\\\\Model\\\\PaymentInterface\\)\\: bool, Closure\\(Sylius\\\\Component\\\\Core\\\\Model\\\\PaymentInterface\\)\\: bool given\\.$#" - count: 1 - path: src/Sylius/Bundle/ShopBundle/Twig/OrderPaymentsExtension.php - - message: "#^Method Sylius\\\\Bundle\\\\TaxationBundle\\\\Form\\\\Type\\\\TaxCalculatorChoiceType\\:\\:__construct\\(\\) has parameter \\$calculators with no value type specified in iterable type array\\.$#" count: 1 @@ -4361,19 +4976,14 @@ parameters: path: src/Sylius/Bundle/UiBundle/Twig/TemplateEventExtension.php - - message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Command\\\\AbstractRoleCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$securityRoles with no value type specified in iterable type array\\.$#" + message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Console\\\\Command\\\\AbstractRoleCommand\\:\\:getUserRepository\\(\\) return type with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/UserBundle/Command/AbstractRoleCommand.php - - - - message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Command\\\\AbstractRoleCommand\\:\\:getAvailableUserTypes\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Bundle/UserBundle/Command/AbstractRoleCommand.php + path: src/Sylius/Bundle/UserBundle/Console/Command/AbstractRoleCommand.php - message: "#^Unable to resolve the template type T in call to method Doctrine\\\\Persistence\\\\ObjectManager\\:\\:getRepository\\(\\)$#" count: 1 - path: src/Sylius/Bundle/UserBundle/Command/AbstractRoleCommand.php + path: src/Sylius/Bundle/UserBundle/Console/Command/AbstractRoleCommand.php - message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Command\\\\DemoteUserCommand\\:\\:executeRoleCommand\\(\\) has parameter \\$securityRoles with no value type specified in iterable type array\\.$#" @@ -4395,6 +5005,11 @@ parameters: count: 1 path: src/Sylius/Bundle/UserBundle/Controller/UserController.php + - + message: "#^PHPDoc tag @var for variable \\$userRepository contains generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Bundle/UserBundle/Controller/UserController.php + - message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\DependencyInjection\\\\SyliusUserExtension\\:\\:createLastLoginListeners\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#" count: 1 @@ -4475,11 +5090,6 @@ parameters: count: 1 path: src/Sylius/Bundle/UserBundle/EventListener/UserLastLoginSubscriber.php - - - message: "#^Class Sylius\\\\Bundle\\\\UserBundle\\\\Factory\\\\UserWithEncoderFactory implements generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Bundle/UserBundle/Factory/UserWithEncoderFactory.php - - message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Factory\\\\UserWithEncoderFactory\\:\\:__construct\\(\\) has parameter \\$decoratedUserFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -4491,9 +5101,9 @@ parameters: path: src/Sylius/Bundle/UserBundle/Form/UserVerifiedAtToBooleanTransformer.php - - message: "#^Interface Sylius\\\\Bundle\\\\UserBundle\\\\Provider\\\\UserProviderInterface extends generic interface Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserProviderInterface but does not specify its types\\: TUser$#" + message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Provider\\\\AbstractUserProvider\\:\\:__construct\\(\\) has parameter \\$userRepository with generic interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Bundle/UserBundle/Provider/UserProviderInterface.php + path: src/Sylius/Bundle/UserBundle/Provider/AbstractUserProvider.php - message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Provider\\\\AbstractUserProvider\\:\\:loadUserByUsername\\(\\) has parameter \\$username with no type specified\\.$#" @@ -4505,6 +5115,11 @@ parameters: count: 1 path: src/Sylius/Bundle/UserBundle/Provider/AbstractUserProvider.php + - + message: "#^Interface Sylius\\\\Bundle\\\\UserBundle\\\\Provider\\\\UserProviderInterface extends generic interface Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserProviderInterface but does not specify its types\\: TUser$#" + count: 1 + path: src/Sylius/Bundle/UserBundle/Provider/UserProviderInterface.php + - message: "#^Method Sylius\\\\Bundle\\\\UserBundle\\\\Provider\\\\UserProviderInterface\\:\\:loadUserByUsername\\(\\) has parameter \\$username with no type specified\\.$#" count: 1 @@ -4555,56 +5170,11 @@ parameters: count: 1 path: src/Sylius/Component/Addressing/Factory/ZoneFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Addressing\\\\Factory\\\\ZoneFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Addressing/Factory/ZoneFactoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Addressing\\\\Factory\\\\ZoneFactoryInterface\\:\\:createWithMembers\\(\\) has parameter \\$membersCodes with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Addressing/Factory/ZoneFactoryInterface.php - - - message: "#^Constant Sylius\\\\Component\\\\Addressing\\\\Matcher\\\\ZoneMatcher\\:\\:PRIORITIES type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Component/Addressing/Matcher/ZoneMatcher.php - - - - message: "#^Method Sylius\\\\Component\\\\Addressing\\\\Matcher\\\\ZoneMatcher\\:\\:__construct\\(\\) has parameter \\$zoneRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Addressing/Matcher/ZoneMatcher.php - - - - message: "#^Method Sylius\\\\Component\\\\Addressing\\\\Matcher\\\\ZoneMatcher\\:\\:getZones\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Component/Addressing/Matcher/ZoneMatcher.php - - - - message: "#^Method Sylius\\\\Component\\\\Addressing\\\\Model\\\\Address\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Addressing/Model/Address.php - - - - message: "#^Method Sylius\\\\Component\\\\Addressing\\\\Model\\\\Country\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Addressing/Model/Country.php - - - - message: "#^Method Sylius\\\\Component\\\\Addressing\\\\Model\\\\Province\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Addressing/Model/Province.php - - - - message: "#^Method Sylius\\\\Component\\\\Addressing\\\\Model\\\\Zone\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Addressing/Model/Zone.php - - - - message: "#^Method Sylius\\\\Component\\\\Addressing\\\\Model\\\\ZoneMember\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Addressing/Model/ZoneMember.php - - message: "#^Method Sylius\\\\Component\\\\Addressing\\\\Provider\\\\ProvinceNamingProvider\\:\\:__construct\\(\\) has parameter \\$provinceRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -4630,6 +5200,11 @@ parameters: count: 1 path: src/Sylius/Component/Attribute/AttributeType/DatetimeAttributeType.php + - + message: "#^Method Sylius\\\\Component\\\\Attribute\\\\AttributeType\\\\FloatAttributeType\\:\\:validate\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Attribute/AttributeType/FloatAttributeType.php + - message: "#^Method Sylius\\\\Component\\\\Attribute\\\\AttributeType\\\\IntegerAttributeType\\:\\:validate\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -4675,21 +5250,11 @@ parameters: count: 1 path: src/Sylius/Component/Attribute/Factory/AttributeFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Attribute\\\\Factory\\\\AttributeFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Attribute/Factory/AttributeFactoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Attribute\\\\Model\\\\Attribute\\:\\:getConfiguration\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Attribute/Model/Attribute.php - - - message: "#^Method Sylius\\\\Component\\\\Attribute\\\\Model\\\\Attribute\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Attribute/Model/Attribute.php - - message: "#^Method Sylius\\\\Component\\\\Attribute\\\\Model\\\\Attribute\\:\\:setConfiguration\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -4705,21 +5270,11 @@ parameters: count: 1 path: src/Sylius/Component/Attribute/Model/AttributeInterface.php - - - message: "#^Method Sylius\\\\Component\\\\Attribute\\\\Model\\\\AttributeTranslation\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Attribute/Model/AttributeTranslation.php - - message: "#^Method Sylius\\\\Component\\\\Attribute\\\\Model\\\\AttributeValue\\:\\:assertAttributeIsSet\\(\\) has no return type specified\\.$#" count: 1 path: src/Sylius/Component/Attribute/Model/AttributeValue.php - - - message: "#^Method Sylius\\\\Component\\\\Attribute\\\\Model\\\\AttributeValue\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Attribute/Model/AttributeValue.php - - message: "#^Method Sylius\\\\Component\\\\Attribute\\\\Model\\\\AttributeValue\\:\\:getJson\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -4775,23 +5330,23 @@ parameters: count: 1 path: src/Sylius/Component/Channel/Context/RequestBased/CompositeRequestResolver.php + - + message: "#^Method Sylius\\\\Component\\\\Channel\\\\Context\\\\RequestBased\\\\HostnameBasedRequestResolver\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Channel/Context/RequestBased/HostnameBasedRequestResolver.php + + - + message: "#^Method Sylius\\\\Component\\\\Channel\\\\Context\\\\SingleChannelContext\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Channel/Context/SingleChannelContext.php + - message: "#^Method Sylius\\\\Component\\\\Channel\\\\Factory\\\\ChannelFactory\\:\\:__construct\\(\\) has parameter \\$defaultFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Component/Channel/Factory/ChannelFactory.php - - message: "#^Interface Sylius\\\\Component\\\\Channel\\\\Factory\\\\ChannelFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Channel/Factory/ChannelFactoryInterface.php - - - - message: "#^Method Sylius\\\\Component\\\\Channel\\\\Model\\\\Channel\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Channel/Model/Channel.php - - - - message: "#^Interface Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" + message: "#^Method Sylius\\\\Component\\\\Channel\\\\Repository\\\\ChannelRepositoryInterface\\:\\:findAllWithBasicData\\(\\) return type has no value type specified in iterable type iterable\\.$#" count: 1 path: src/Sylius/Component/Channel/Repository/ChannelRepositoryInterface.php @@ -4800,6 +5355,11 @@ parameters: count: 1 path: src/Sylius/Component/Core/Calculator/ProductVariantPriceCalculator.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Calculator\\\\ProductVariantPriceCalculator\\:\\:calculateLowestPriceBeforeDiscount\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Calculator/ProductVariantPriceCalculator.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Calculator\\\\ProductVariantPriceCalculator\\:\\:calculateOriginal\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" count: 1 @@ -4815,11 +5375,56 @@ parameters: count: 1 path: src/Sylius/Component/Core/Calculator/ProductVariantPricesCalculatorInterface.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Checker\\\\ProductVariantLowestPriceDisplayChecker\\:\\:isAnyTaxonExcluded\\(\\) has parameter \\$excludedTaxons with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Checker/ProductVariantLowestPriceDisplayChecker.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Checker\\\\ProductVariantLowestPriceDisplayChecker\\:\\:isAnyTaxonExcluded\\(\\) has parameter \\$taxons with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Checker/ProductVariantLowestPriceDisplayChecker.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Checker\\\\ProductVariantLowestPriceDisplayChecker\\:\\:isCommonPart\\(\\) has parameter \\$firstArray with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Checker/ProductVariantLowestPriceDisplayChecker.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Checker\\\\ProductVariantLowestPriceDisplayChecker\\:\\:isCommonPart\\(\\) has parameter \\$secondArray with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Checker/ProductVariantLowestPriceDisplayChecker.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Checker\\\\ProductVariantLowestPriceDisplayChecker\\:\\:isLowestPriceDisplayable\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Checker/ProductVariantLowestPriceDisplayChecker.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Checker\\\\ProductVariantLowestPriceDisplayCheckerInterface\\:\\:isLowestPriceDisplayable\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Checker/ProductVariantLowestPriceDisplayCheckerInterface.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Customer\\\\Statistics\\\\CustomerStatisticsProvider\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Component/Core/Customer/Statistics/CustomerStatisticsProvider.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Customer\\\\Statistics\\\\CustomerStatisticsProvider\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Customer/Statistics/CustomerStatisticsProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Dashboard\\\\DashboardStatisticsProvider\\:\\:__construct\\(\\) has parameter \\$customerRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\CustomerRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Dashboard/DashboardStatisticsProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Dashboard\\\\DashboardStatisticsProvider\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Dashboard/DashboardStatisticsProvider.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Dashboard\\\\SalesDataProvider\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic class Doctrine\\\\ORM\\\\EntityRepository but does not specify its types\\: TEntityClass$#" count: 1 @@ -4890,16 +5495,6 @@ parameters: count: 1 path: src/Sylius/Component/Core/Distributor/MinimumPriceDistributorInterface.php - - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Distributor\\\\ProportionalIntegerDistributor\\:\\:distribute\\(\\) has parameter \\$integers with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Component/Core/Distributor/ProportionalIntegerDistributor.php - - - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Distributor\\\\ProportionalIntegerDistributor\\:\\:distribute\\(\\) return type has no value type specified in iterable type array\\.$#" - count: 1 - path: src/Sylius/Component/Core/Distributor/ProportionalIntegerDistributor.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Distributor\\\\ProportionalIntegerDistributorInterface\\:\\:distribute\\(\\) has parameter \\$integers with no value type specified in iterable type array\\.$#" count: 1 @@ -4915,66 +5510,21 @@ parameters: count: 1 path: src/Sylius/Component/Core/Factory/AddressFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Factory\\\\AddressFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Factory/AddressFactoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Factory\\\\CartItemFactory\\:\\:__construct\\(\\) has parameter \\$decoratedFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Component/Core/Factory/CartItemFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Factory\\\\CartItemFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Factory/CartItemFactoryInterface.php - - - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Factory\\\\ChannelFactory\\:\\:__construct\\(\\) has parameter \\$decoratedFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Factory/ChannelFactory.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Factory\\\\CustomerAfterCheckoutFactory\\:\\:__construct\\(\\) has parameter \\$baseCustomerFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Component/Core/Factory/CustomerAfterCheckoutFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Factory\\\\CustomerAfterCheckoutFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Factory/CustomerAfterCheckoutFactoryInterface.php - - - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Factory\\\\PaymentMethodFactory\\:\\:__construct\\(\\) has parameter \\$decoratedFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Factory/PaymentMethodFactory.php - - - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Factory\\\\PaymentMethodFactory\\:\\:__construct\\(\\) has parameter \\$gatewayConfigFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Factory/PaymentMethodFactory.php - - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Factory\\\\PaymentMethodFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Factory/PaymentMethodFactoryInterface.php - - - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Factory\\\\PromotionActionFactory\\:\\:__construct\\(\\) has parameter \\$decoratedFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Factory/PromotionActionFactory.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Factory\\\\PromotionActionFactory\\:\\:createAction\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Core/Factory/PromotionActionFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Factory\\\\PromotionActionFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Factory/PromotionActionFactoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Factory\\\\PromotionRuleFactory\\:\\:__construct\\(\\) has parameter \\$decoratedFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -4990,11 +5540,6 @@ parameters: count: 1 path: src/Sylius/Component/Core/Factory/PromotionRuleFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Factory\\\\PromotionRuleFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Factory/PromotionRuleFactoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Factory\\\\PromotionRuleFactoryInterface\\:\\:createHasTaxon\\(\\) has parameter \\$taxons with no value type specified in iterable type array\\.$#" count: 1 @@ -5016,12 +5561,12 @@ parameters: path: src/Sylius/Component/Core/Model/AdminUserInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Model\\\\ChannelPricing\\:\\:getAppliedPromotions\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#" + message: "#^Method Sylius\\\\Component\\\\Core\\\\Model\\\\ChannelPriceHistoryConfigInterface\\:\\:getTaxonsExcludedFromShowingLowestPrice\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#" count: 1 - path: src/Sylius/Component/Core/Model/ChannelPricing.php + path: src/Sylius/Component/Core/Model/ChannelPriceHistoryConfigInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Model\\\\ChannelPricing\\:\\:getId\\(\\) has no return type specified\\.$#" + message: "#^Method Sylius\\\\Component\\\\Core\\\\Model\\\\ChannelPricing\\:\\:getAppliedPromotions\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#" count: 1 path: src/Sylius/Component/Core/Model/ChannelPricing.php @@ -5035,26 +5580,11 @@ parameters: count: 1 path: src/Sylius/Component/Core/Model/CustomerInterface.php - - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Model\\\\Image\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Core/Model/Image.php - - - - message: "#^Parameter \\#1 \\$element of method Doctrine\\\\Common\\\\Collections\\\\ArrayCollection\\\\:\\:add\\(\\) expects Sylius\\\\Component\\\\Core\\\\Model\\\\OrderItemUnitInterface, Sylius\\\\Component\\\\Order\\\\Model\\\\OrderItemUnitInterface given\\.$#" - count: 1 - path: src/Sylius/Component/Core/Model/Order.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Model\\\\Product\\:\\:getVariantSelectionMethodLabels\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Core/Model/Product.php - - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Model\\\\ProductTaxon\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Core/Model/ProductTaxon.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Model\\\\ProductVariant\\:\\:getAppliedPromotionsForChannel\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#" count: 1 @@ -5070,16 +5600,6 @@ parameters: count: 1 path: src/Sylius/Component/Core/Model/ProductVariantInterface.php - - - message: "#^Parameter \\#1 \\$p of method Doctrine\\\\Common\\\\Collections\\\\Collection\\<\\(int\\|string\\),Sylius\\\\Component\\\\Order\\\\Model\\\\AdjustmentInterface\\>\\:\\:filter\\(\\) expects Closure\\(Sylius\\\\Component\\\\Order\\\\Model\\\\AdjustmentInterface\\)\\: bool, Closure\\(Sylius\\\\Component\\\\Core\\\\Model\\\\AdjustmentInterface\\)\\: bool given\\.$#" - count: 1 - path: src/Sylius/Component/Core/Model/Shipment.php - - - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Model\\\\ShopBillingData\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Core/Model/ShopBillingData.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\OrderProcessing\\\\OrderAdjustmentsClearer\\:\\:__construct\\(\\) has parameter \\$adjustmentsToRemove with no value type specified in iterable type array\\.$#" count: 1 @@ -5090,11 +5610,6 @@ parameters: count: 1 path: src/Sylius/Component/Core/OrderProcessing/OrderAdjustmentsClearer.php - - - message: "#^Parameter \\#1 \\$p of method Doctrine\\\\Common\\\\Collections\\\\Collection\\<\\(int\\|string\\),Sylius\\\\Component\\\\Payment\\\\Model\\\\PaymentInterface\\>\\:\\:filter\\(\\) expects Closure\\(Sylius\\\\Component\\\\Payment\\\\Model\\\\PaymentInterface\\)\\: bool, Closure\\(Sylius\\\\Component\\\\Core\\\\Model\\\\PaymentInterface\\)\\: bool given\\.$#" - count: 1 - path: src/Sylius/Component/Core/OrderProcessing/OrderPaymentProcessor.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\OrderProcessing\\\\OrderShipmentProcessor\\:\\:__construct\\(\\) has parameter \\$shipmentFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -5105,6 +5620,11 @@ parameters: count: 1 path: src/Sylius/Component/Core/OrderProcessing/ShippingChargesProcessor.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Payment\\\\Provider\\\\OrderPaymentProvider\\:\\:__construct\\(\\) has parameter \\$paymentFactory with generic interface Sylius\\\\Component\\\\Payment\\\\Factory\\\\PaymentFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Payment/Provider/OrderPaymentProvider.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Action\\\\DiscountPromotionActionCommand\\:\\:isConfigurationValid\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -5115,11 +5635,6 @@ parameters: count: 1 path: src/Sylius/Component/Core/Promotion/Action/DiscountPromotionActionCommand.php - - - message: "#^Parameter \\#1 \\$unit of method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Action\\\\DiscountPromotionActionCommand\\:\\:removeUnitOrderPromotionAdjustmentsByOrigin\\(\\) expects Sylius\\\\Component\\\\Core\\\\Model\\\\OrderItemUnitInterface, Sylius\\\\Component\\\\Order\\\\Model\\\\OrderItemUnitInterface given\\.$#" - count: 1 - path: src/Sylius/Component/Core/Promotion/Action/DiscountPromotionActionCommand.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Action\\\\FixedDiscountPromotionActionCommand\\:\\:execute\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -5165,11 +5680,6 @@ parameters: count: 1 path: src/Sylius/Component/Core/Promotion/Action/UnitDiscountPromotionActionCommand.php - - - message: "#^Parameter \\#1 \\$unit of method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Action\\\\UnitDiscountPromotionActionCommand\\:\\:removeUnitOrderItemAdjustments\\(\\) expects Sylius\\\\Component\\\\Core\\\\Model\\\\OrderItemUnitInterface, Sylius\\\\Component\\\\Order\\\\Model\\\\OrderItemUnitInterface given\\.$#" - count: 1 - path: src/Sylius/Component/Core/Promotion/Action/UnitDiscountPromotionActionCommand.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Action\\\\UnitFixedDiscountPromotionActionCommand\\:\\:__construct\\(\\) has parameter \\$adjustmentFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -5180,11 +5690,6 @@ parameters: count: 1 path: src/Sylius/Component/Core/Promotion/Action/UnitFixedDiscountPromotionActionCommand.php - - - message: "#^Parameter \\#1 \\$unit of method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Action\\\\UnitDiscountPromotionActionCommand\\:\\:addAdjustmentToUnit\\(\\) expects Sylius\\\\Component\\\\Core\\\\Model\\\\OrderItemUnitInterface, Sylius\\\\Component\\\\Order\\\\Model\\\\OrderItemUnitInterface given\\.$#" - count: 1 - path: src/Sylius/Component/Core/Promotion/Action/UnitFixedDiscountPromotionActionCommand.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Action\\\\UnitPercentageDiscountPromotionActionCommand\\:\\:__construct\\(\\) has parameter \\$adjustmentFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 @@ -5196,9 +5701,14 @@ parameters: path: src/Sylius/Component/Core/Promotion/Action/UnitPercentageDiscountPromotionActionCommand.php - - message: "#^Parameter \\#1 \\$unit of method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Action\\\\UnitDiscountPromotionActionCommand\\:\\:addAdjustmentToUnit\\(\\) expects Sylius\\\\Component\\\\Core\\\\Model\\\\OrderItemUnitInterface, Sylius\\\\Component\\\\Order\\\\Model\\\\OrderItemUnitInterface given\\.$#" + message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Applicator\\\\UnitsPromotionAdjustmentsApplicator\\:\\:__construct\\(\\) has parameter \\$adjustmentFactory with generic interface Sylius\\\\Component\\\\Order\\\\Factory\\\\AdjustmentFactoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Component/Core/Promotion/Action/UnitPercentageDiscountPromotionActionCommand.php + path: src/Sylius/Component/Core/Promotion/Applicator/UnitsPromotionAdjustmentsApplicator.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Checker\\\\Eligibility\\\\PromotionCouponPerCustomerUsageLimitEligibilityChecker\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Promotion/Checker/Eligibility/PromotionCouponPerCustomerUsageLimitEligibilityChecker.php - message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Checker\\\\Rule\\\\ContainsProductRuleChecker\\:\\:isEligible\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" @@ -5225,6 +5735,11 @@ parameters: count: 1 path: src/Sylius/Component/Core/Promotion/Checker/Rule/ItemTotalRuleChecker.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Checker\\\\Rule\\\\NthOrderRuleChecker\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Promotion/Checker/Rule/NthOrderRuleChecker.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Checker\\\\Rule\\\\NthOrderRuleChecker\\:\\:isEligible\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -5240,6 +5755,11 @@ parameters: count: 1 path: src/Sylius/Component/Core/Promotion/Checker/Rule/ShippingCountryRuleChecker.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Checker\\\\Rule\\\\TotalOfItemsFromTaxonRuleChecker\\:\\:__construct\\(\\) has parameter \\$taxonRepository with generic interface Sylius\\\\Component\\\\Taxonomy\\\\Repository\\\\TaxonRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Promotion/Checker/Rule/TotalOfItemsFromTaxonRuleChecker.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Promotion\\\\Checker\\\\Rule\\\\TotalOfItemsFromTaxonRuleChecker\\:\\:isEligible\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -5315,11 +5835,141 @@ parameters: count: 1 path: src/Sylius/Component/Core/Promotion/Updater/Rule/TotalOfItemsFromTaxonRuleUpdater.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ActivePromotionsByChannelProvider\\:\\:__construct\\(\\) has parameter \\$promotionRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PromotionRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ActivePromotionsByChannelProvider.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\CatalogPromotionVariantsProviderInterface\\:\\:provideEligibleVariants\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Core/Provider/CatalogPromotionVariantsProviderInterface.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantAppliedPromotionsMapProvider\\:\\:provide\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantAppliedPromotionsMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantAppliedPromotionsMapProvider\\:\\:provide\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantAppliedPromotionsMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantAppliedPromotionsMapProvider\\:\\:supports\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantAppliedPromotionsMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantLowestPriceMapProvider\\:\\:provide\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantLowestPriceMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantLowestPriceMapProvider\\:\\:provide\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantLowestPriceMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantLowestPriceMapProvider\\:\\:supports\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantLowestPriceMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantMapProviderInterface\\:\\:provide\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantMapProviderInterface.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantMapProviderInterface\\:\\:provide\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantMapProviderInterface.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantMapProviderInterface\\:\\:supports\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantMapProviderInterface.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantOptionsMapProvider\\:\\:provide\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantOptionsMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantOptionsMapProvider\\:\\:provide\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantOptionsMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantOptionsMapProvider\\:\\:supports\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantOptionsMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantOriginalPriceMapProvider\\:\\:isPriceLowerThanOriginalPrice\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantOriginalPriceMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantOriginalPriceMapProvider\\:\\:provide\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantOriginalPriceMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantOriginalPriceMapProvider\\:\\:provide\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantOriginalPriceMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantOriginalPriceMapProvider\\:\\:supports\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantOriginalPriceMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantPriceMapProvider\\:\\:provide\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantPriceMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantPriceMapProvider\\:\\:provide\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantPriceMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantPriceMapProvider\\:\\:supports\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantPriceMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantsMapProvider\\:\\:getMapForVariant\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantsMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantsMapProvider\\:\\:getMapForVariant\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantsMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantsMapProvider\\:\\:provide\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantsMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantsMapProvider\\:\\:provide\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantsMapProvider.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantsMapProviderInterface\\:\\:provide\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantsMapProviderInterface.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantMap\\\\ProductVariantsMapProviderInterface\\:\\:provide\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Provider/ProductVariantMap/ProductVariantsMapProviderInterface.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Provider\\\\ProductVariantsPricesProvider\\:\\:constructOptionsMap\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -5345,31 +5995,11 @@ parameters: count: 1 path: src/Sylius/Component/Core/Provider/TranslationLocaleProvider.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Repository\\\\AddressRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Repository/AddressRepositoryInterface.php - - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Repository\\\\AvatarImageRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Repository/AvatarImageRepositoryInterface.php - - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Repository\\\\CustomerRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Repository/CustomerRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderItemRepositoryInterface\\:\\:findOneByIdAndCustomer\\(\\) has parameter \\$id with no type specified\\.$#" count: 1 path: src/Sylius/Component/Core/Repository/OrderItemRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderItemUnitRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Repository/OrderItemUnitRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderItemUnitRepositoryInterface\\:\\:findOneByCustomer\\(\\) has parameter \\$id with no type specified\\.$#" count: 1 @@ -5430,11 +6060,6 @@ parameters: count: 1 path: src/Sylius/Component/Core/Repository/PaymentMethodRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Repository/PaymentRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentRepositoryInterface\\:\\:findOneByCustomer\\(\\) has parameter \\$id with no type specified\\.$#" count: 1 @@ -5450,11 +6075,6 @@ parameters: count: 1 path: src/Sylius/Component/Core/Repository/PaymentRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductAssociationRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Repository/ProductAssociationRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductAssociationRepositoryInterface\\:\\:findWithProductsWithinChannel\\(\\) has parameter \\$associationId with no type specified\\.$#" count: 1 @@ -5470,11 +6090,6 @@ parameters: count: 1 path: src/Sylius/Component/Core/Repository/ProductRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductReviewRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductReviewRepositoryInterface\\:\\:findLatestByProductId\\(\\) has parameter \\$productId with no type specified\\.$#" count: 1 @@ -5485,21 +6100,11 @@ parameters: count: 1 path: src/Sylius/Component/Core/Repository/ProductReviewRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductTaxonRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Repository/ProductTaxonRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Repository\\\\ProductVariantRepositoryInterface\\:\\:findByTaxon\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Core/Repository/ProductVariantRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Repository/ShipmentRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Repository\\\\ShipmentRepositoryInterface\\:\\:findOneByCustomer\\(\\) has parameter \\$id with no type specified\\.$#" count: 1 @@ -5520,16 +6125,36 @@ parameters: count: 1 path: src/Sylius/Component/Core/Repository/ShipmentRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShippingCategoryRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Core/Repository/ShippingCategoryRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Repository\\\\ShippingMethodRepositoryInterface\\:\\:findEnabledForZonesAndChannel\\(\\) has parameter \\$zones with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Core/Repository/ShippingMethodRepositoryInterface.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Resolver\\\\ChannelBasedPaymentMethodsResolver\\:\\:__construct\\(\\) has parameter \\$paymentMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Resolver/ChannelBasedPaymentMethodsResolver.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Resolver\\\\DefaultPaymentMethodResolver\\:\\:__construct\\(\\) has parameter \\$paymentMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\PaymentMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Resolver/DefaultPaymentMethodResolver.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Resolver\\\\DefaultShippingMethodResolver\\:\\:__construct\\(\\) has parameter \\$shippingMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShippingMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Resolver/DefaultShippingMethodResolver.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Resolver\\\\EligibleDefaultShippingMethodResolver\\:\\:__construct\\(\\) has parameter \\$shippingMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShippingMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Resolver/EligibleDefaultShippingMethodResolver.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Resolver\\\\ZoneAndChannelBasedShippingMethodsResolver\\:\\:__construct\\(\\) has parameter \\$shippingMethodRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\ShippingMethodRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Resolver/ZoneAndChannelBasedShippingMethodsResolver.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Shipping\\\\Calculator\\\\FlatRateCalculator\\:\\:calculate\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -5546,24 +6171,24 @@ parameters: path: src/Sylius/Component/Core/Shipping/Checker/Rule/OrderTotalRuleChecker.php - - message: "#^Parameter \\#1 \\$p of method Doctrine\\\\Common\\\\Collections\\\\Collection\\<\\(int\\|string\\),Sylius\\\\Component\\\\Payment\\\\Model\\\\PaymentInterface\\>\\:\\:filter\\(\\) expects Closure\\(Sylius\\\\Component\\\\Payment\\\\Model\\\\PaymentInterface\\)\\: bool, Closure\\(Sylius\\\\Component\\\\Core\\\\Model\\\\PaymentInterface\\)\\: bool given\\.$#" - count: 1 - path: src/Sylius/Component/Core/StateResolver/OrderPaymentStateResolver.php - - - - message: "#^Parameter \\#1 \\$unit of method Sylius\\\\Component\\\\Core\\\\Taxation\\\\Applicator\\\\OrderItemUnitsTaxesApplicator\\:\\:addAdjustment\\(\\) expects Sylius\\\\Component\\\\Core\\\\Model\\\\OrderItemUnitInterface, Sylius\\\\Component\\\\Order\\\\Model\\\\OrderItemUnitInterface given\\.$#" + message: "#^Method Sylius\\\\Component\\\\Core\\\\Taxation\\\\Applicator\\\\OrderItemUnitsTaxesApplicator\\:\\:__construct\\(\\) has parameter \\$adjustmentFactory with generic interface Sylius\\\\Component\\\\Order\\\\Factory\\\\AdjustmentFactoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Component/Core/Taxation/Applicator/OrderItemUnitsTaxesApplicator.php - - message: "#^Parameter \\#1 \\$unit of method Sylius\\\\Component\\\\Core\\\\Taxation\\\\Applicator\\\\OrderItemsTaxesApplicator\\:\\:addAdjustment\\(\\) expects Sylius\\\\Component\\\\Core\\\\Model\\\\OrderItemUnitInterface, Sylius\\\\Component\\\\Order\\\\Model\\\\OrderItemUnitInterface given\\.$#" + message: "#^Method Sylius\\\\Component\\\\Core\\\\Taxation\\\\Applicator\\\\OrderItemsTaxesApplicator\\:\\:__construct\\(\\) has parameter \\$adjustmentFactory with generic interface Sylius\\\\Component\\\\Order\\\\Factory\\\\AdjustmentFactoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Component/Core/Taxation/Applicator/OrderItemsTaxesApplicator.php - - message: "#^Method Sylius\\\\Component\\\\Core\\\\Test\\\\Factory\\\\TestPromotionFactory\\:\\:__construct\\(\\) has parameter \\$promotionFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" + message: "#^Method Sylius\\\\Component\\\\Core\\\\Taxation\\\\Applicator\\\\OrderShipmentTaxesApplicator\\:\\:__construct\\(\\) has parameter \\$adjustmentFactory with generic interface Sylius\\\\Component\\\\Order\\\\Factory\\\\AdjustmentFactoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Component/Core/Test/Factory/TestPromotionFactory.php + path: src/Sylius/Component/Core/Taxation/Applicator/OrderShipmentTaxesApplicator.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Test\\\\Services\\\\DefaultChannelFactory\\:\\:__construct\\(\\) has parameter \\$channelFactory with generic interface Sylius\\\\Component\\\\Channel\\\\Factory\\\\ChannelFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Test/Services/DefaultChannelFactory.php - message: "#^Method Sylius\\\\Component\\\\Core\\\\Test\\\\Services\\\\DefaultChannelFactory\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" @@ -5600,6 +6225,11 @@ parameters: count: 1 path: src/Sylius/Component/Core/Test/Services/DefaultChannelFactoryInterface.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Test\\\\Services\\\\DefaultUnitedStatesChannelFactory\\:\\:__construct\\(\\) has parameter \\$channelFactory with generic interface Sylius\\\\Component\\\\Channel\\\\Factory\\\\ChannelFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Test/Services/DefaultUnitedStatesChannelFactory.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Test\\\\Services\\\\DefaultUnitedStatesChannelFactory\\:\\:__construct\\(\\) has parameter \\$channelRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -5635,6 +6265,11 @@ parameters: count: 1 path: src/Sylius/Component/Core/Test/Services/DefaultUnitedStatesChannelFactory.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Test\\\\Services\\\\DefaultUnitedStatesChannelFactory\\:\\:__construct\\(\\) has parameter \\$zoneFactory with generic interface Sylius\\\\Component\\\\Addressing\\\\Factory\\\\ZoneFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Test/Services/DefaultUnitedStatesChannelFactory.php + - message: "#^Method Sylius\\\\Component\\\\Core\\\\Test\\\\Services\\\\DefaultUnitedStatesChannelFactory\\:\\:__construct\\(\\) has parameter \\$zoneRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -5645,20 +6280,25 @@ parameters: count: 1 path: src/Sylius/Component/Core/Test/Services/DefaultUnitedStatesChannelFactory.php + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Updater\\\\UnpaidOrdersStateUpdater\\:\\:__construct\\(\\) has parameter \\$orderRepository with generic interface Sylius\\\\Component\\\\Core\\\\Repository\\\\OrderRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Core/Updater/UnpaidOrdersStateUpdater.php + + - + message: "#^Method Sylius\\\\Component\\\\Core\\\\Updater\\\\UnpaidOrdersStateUpdater\\:\\:findExpiredUnpaidOrders\\(\\) return type has no value type specified in iterable type array\\.$#" + count: 1 + path: src/Sylius/Component/Core/Updater/UnpaidOrdersStateUpdater.php + - message: "#^Property Sylius\\\\Component\\\\Currency\\\\Context\\\\CompositeCurrencyContext\\:\\:\\$currencyContexts with generic class Laminas\\\\Stdlib\\\\PriorityQueue does not specify its types\\: TValue, TPriority$#" count: 1 path: src/Sylius/Component/Currency/Context/CompositeCurrencyContext.php - - message: "#^Method Sylius\\\\Component\\\\Currency\\\\Model\\\\Currency\\:\\:getId\\(\\) has no return type specified\\.$#" + message: "#^Method Sylius\\\\Component\\\\Currency\\\\Converter\\\\CurrencyConverter\\:\\:__construct\\(\\) has parameter \\$exchangeRateRepository with generic interface Sylius\\\\Component\\\\Currency\\\\Repository\\\\ExchangeRateRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Component/Currency/Model/Currency.php - - - - message: "#^Method Sylius\\\\Component\\\\Currency\\\\Model\\\\ExchangeRate\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Currency/Model/ExchangeRate.php + path: src/Sylius/Component/Currency/Converter/CurrencyConverter.php - message: "#^Method Sylius\\\\Component\\\\Currency\\\\Model\\\\ExchangeRateInterface\\:\\:setRatio\\(\\) has no return type specified\\.$#" @@ -5666,24 +6306,9 @@ parameters: path: src/Sylius/Component/Currency/Model/ExchangeRateInterface.php - - message: "#^Interface Sylius\\\\Component\\\\Currency\\\\Repository\\\\ExchangeRateRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" + message: "#^Method Sylius\\\\Component\\\\Customer\\\\Repository\\\\CustomerGroupRepositoryInterface\\:\\:findByPhrase\\(\\) return type has no value type specified in iterable type iterable\\.$#" count: 1 - path: src/Sylius/Component/Currency/Repository/ExchangeRateRepositoryInterface.php - - - - message: "#^Method Sylius\\\\Component\\\\Customer\\\\Model\\\\Customer\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Customer/Model/Customer.php - - - - message: "#^Method Sylius\\\\Component\\\\Customer\\\\Model\\\\CustomerGroup\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Customer/Model/CustomerGroup.php - - - - message: "#^Method Sylius\\\\Component\\\\Inventory\\\\Model\\\\InventoryUnit\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Inventory/Model/InventoryUnit.php + path: src/Sylius/Component/Customer/Repository/CustomerGroupRepositoryInterface.php - message: "#^Property Sylius\\\\Component\\\\Locale\\\\Context\\\\CompositeLocaleContext\\:\\:\\$localeContexts with generic class Laminas\\\\Stdlib\\\\PriorityQueue does not specify its types\\: TValue, TPriority$#" @@ -5695,11 +6320,6 @@ parameters: count: 1 path: src/Sylius/Component/Locale/Context/LocaleNotFoundException.php - - - message: "#^Method Sylius\\\\Component\\\\Locale\\\\Model\\\\Locale\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Locale/Model/Locale.php - - message: "#^Method Sylius\\\\Component\\\\Locale\\\\Provider\\\\LocaleProvider\\:\\:__construct\\(\\) has parameter \\$localeRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -5735,31 +6355,16 @@ parameters: count: 1 path: src/Sylius/Component/Order/Factory/AdjustmentFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Order\\\\Factory\\\\AdjustmentFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Order/Factory/AdjustmentFactoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Order\\\\Factory\\\\AdjustmentFactoryInterface\\:\\:createWithData\\(\\) has parameter \\$details with no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Order/Factory/AdjustmentFactoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Order\\\\Factory\\\\OrderItemUnitFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Order/Factory/OrderItemUnitFactoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Order\\\\Model\\\\Adjustment\\:\\:getDetails\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Order/Model/Adjustment.php - - - message: "#^Method Sylius\\\\Component\\\\Order\\\\Model\\\\Adjustment\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Order/Model/Adjustment.php - - message: "#^Method Sylius\\\\Component\\\\Order\\\\Model\\\\Adjustment\\:\\:setDetails\\(\\) has parameter \\$details with no value type specified in iterable type array\\.$#" count: 1 @@ -5776,35 +6381,15 @@ parameters: path: src/Sylius/Component/Order/Model/AdjustmentInterface.php - - message: "#^Method Sylius\\\\Component\\\\Order\\\\Model\\\\Order\\:\\:getId\\(\\) has no return type specified\\.$#" + message: "#^Method Sylius\\\\Component\\\\Order\\\\Modifier\\\\OrderItemQuantityModifier\\:\\:__construct\\(\\) has parameter \\$orderItemUnitFactory with generic interface Sylius\\\\Component\\\\Order\\\\Factory\\\\OrderItemUnitFactoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Component/Order/Model/Order.php - - - - message: "#^Method Sylius\\\\Component\\\\Order\\\\Model\\\\OrderItem\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Order/Model/OrderItem.php - - - - message: "#^Method Sylius\\\\Component\\\\Order\\\\Model\\\\OrderItemUnit\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Order/Model/OrderItemUnit.php - - - - message: "#^Method Sylius\\\\Component\\\\Order\\\\Model\\\\OrderSequence\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Order/Model/OrderSequence.php + path: src/Sylius/Component/Order/Modifier/OrderItemQuantityModifier.php - message: "#^Property Sylius\\\\Component\\\\Order\\\\Processor\\\\CompositeOrderProcessor\\:\\:\\$orderProcessors with generic class Laminas\\\\Stdlib\\\\PriorityQueue does not specify its types\\: TValue, TPriority$#" count: 1 path: src/Sylius/Component/Order/Processor/CompositeOrderProcessor.php - - - message: "#^Interface Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderItemRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Order/Repository/OrderItemRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderItemRepositoryInterface\\:\\:findOneByIdAndCartId\\(\\) has parameter \\$cartId with no type specified\\.$#" count: 1 @@ -5825,11 +6410,6 @@ parameters: count: 1 path: src/Sylius/Component/Order/Repository/OrderItemRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Order/Repository/OrderRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Order\\\\Repository\\\\OrderRepositoryInterface\\:\\:findAllExceptCarts\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -5845,21 +6425,11 @@ parameters: count: 1 path: src/Sylius/Component/Payment/Factory/PaymentFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Payment\\\\Factory\\\\PaymentFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Payment/Factory/PaymentFactoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Payment\\\\Model\\\\Payment\\:\\:getDetails\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Payment/Model/Payment.php - - - message: "#^Method Sylius\\\\Component\\\\Payment\\\\Model\\\\Payment\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Payment/Model/Payment.php - - message: "#^Method Sylius\\\\Component\\\\Payment\\\\Model\\\\Payment\\:\\:setDetails\\(\\) has parameter \\$details with no value type specified in iterable type array\\.$#" count: 1 @@ -5876,19 +6446,9 @@ parameters: path: src/Sylius/Component/Payment/Model/PaymentInterface.php - - message: "#^Method Sylius\\\\Component\\\\Payment\\\\Model\\\\PaymentMethod\\:\\:getId\\(\\) has no return type specified\\.$#" + message: "#^Method Sylius\\\\Component\\\\Payment\\\\Resolver\\\\DefaultPaymentMethodResolver\\:\\:__construct\\(\\) has parameter \\$paymentMethodRepository with generic interface Sylius\\\\Component\\\\Payment\\\\Repository\\\\PaymentMethodRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Component/Payment/Model/PaymentMethod.php - - - - message: "#^Method Sylius\\\\Component\\\\Payment\\\\Model\\\\PaymentMethodTranslation\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Payment/Model/PaymentMethodTranslation.php - - - - message: "#^Interface Sylius\\\\Component\\\\Payment\\\\Repository\\\\PaymentMethodRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Payment/Repository/PaymentMethodRepositoryInterface.php + path: src/Sylius/Component/Payment/Resolver/DefaultPaymentMethodResolver.php - message: "#^Method Sylius\\\\Component\\\\Payment\\\\Resolver\\\\PaymentMethodsResolver\\:\\:__construct\\(\\) has parameter \\$paymentMethodRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" @@ -5910,11 +6470,6 @@ parameters: count: 1 path: src/Sylius/Component/Product/Factory/ProductVariantFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Product\\\\Factory\\\\ProductVariantFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Product/Factory/ProductVariantFactoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Generator\\\\CartesianSetBuilder\\:\\:build\\(\\) has parameter \\$setTuples with no value type specified in iterable type array\\.$#" count: 1 @@ -5970,6 +6525,11 @@ parameters: count: 1 path: src/Sylius/Component/Product/Generator/CartesianSetBuilder.php + - + message: "#^Method Sylius\\\\Component\\\\Product\\\\Generator\\\\ProductVariantGenerator\\:\\:__construct\\(\\) has parameter \\$productVariantFactory with generic interface Sylius\\\\Component\\\\Product\\\\Factory\\\\ProductVariantFactoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Product/Generator/ProductVariantGenerator.php + - message: "#^Method Sylius\\\\Component\\\\Product\\\\Generator\\\\ProductVariantGenerator\\:\\:addOptionValue\\(\\) has parameter \\$optionMap with no value type specified in iterable type array\\.$#" count: 1 @@ -5990,101 +6550,11 @@ parameters: count: 1 path: src/Sylius/Component/Product/Generator/ProductVariantGenerator.php - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\Product\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/Product.php - - - - message: "#^Parameter \\#1 \\$attributeValue of method Sylius\\\\Component\\\\Product\\\\Model\\\\Product\\:\\:getAttributeInDifferentLocale\\(\\) expects Sylius\\\\Component\\\\Product\\\\Model\\\\ProductAttributeValueInterface, Sylius\\\\Component\\\\Attribute\\\\Model\\\\AttributeValueInterface given\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/Product.php - - - - message: "#^Parameter \\#1 \\$p of method Doctrine\\\\Common\\\\Collections\\\\Collection\\<\\(int\\|string\\),Sylius\\\\Component\\\\Attribute\\\\Model\\\\AttributeValueInterface\\>\\:\\:filter\\(\\) expects Closure\\(Sylius\\\\Component\\\\Attribute\\\\Model\\\\AttributeValueInterface\\)\\: bool, Closure\\(Sylius\\\\Component\\\\Product\\\\Model\\\\ProductAttributeValueInterface\\)\\: bool given\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/Product.php - - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\ProductAssociation\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/ProductAssociation.php - - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\ProductAssociationType\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/ProductAssociationType.php - - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\ProductAssociationTypeTranslation\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/ProductAssociationTypeTranslation.php - - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\ProductOption\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/ProductOption.php - - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\ProductOptionTranslation\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/ProductOptionTranslation.php - - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\ProductOptionValue\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/ProductOptionValue.php - - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\ProductOptionValueTranslation\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/ProductOptionValueTranslation.php - - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\ProductTranslation\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/ProductTranslation.php - - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\ProductVariant\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/ProductVariant.php - - - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Model\\\\ProductVariantTranslation\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Product/Model/ProductVariantTranslation.php - - - - message: "#^Interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductAssociationTypeRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Product/Repository/ProductAssociationTypeRepositoryInterface.php - - - - message: "#^Interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductAttributeValueRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Product/Repository/ProductAttributeValueRepositoryInterface.php - - - - message: "#^Interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductOptionRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Product/Repository/ProductOptionRepositoryInterface.php - - - - message: "#^Interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Product/Repository/ProductRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductRepositoryInterface\\:\\:findByPhrase\\(\\) return type has no value type specified in iterable type iterable\\.$#" count: 1 path: src/Sylius/Component/Product/Repository/ProductRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductVariantRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Product/Repository/ProductVariantRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductVariantRepositoryInterface\\:\\:createQueryBuilderByProductId\\(\\) has parameter \\$productId with no type specified\\.$#" count: 1 @@ -6100,6 +6570,11 @@ parameters: count: 1 path: src/Sylius/Component/Product/Repository/ProductVariantRepositoryInterface.php + - + message: "#^Method Sylius\\\\Component\\\\Product\\\\Resolver\\\\DefaultProductVariantResolver\\:\\:__construct\\(\\) has parameter \\$productVariantRepository with generic interface Sylius\\\\Component\\\\Product\\\\Repository\\\\ProductVariantRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Product/Resolver/DefaultProductVariantResolver.php + - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Action\\\\PromotionActionCommandInterface\\:\\:execute\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -6131,15 +6606,20 @@ parameters: path: src/Sylius/Component/Promotion/Factory/PromotionCouponFactory.php - - message: "#^Interface Sylius\\\\Component\\\\Promotion\\\\Factory\\\\PromotionCouponFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" + message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Generator\\\\PercentageGenerationPolicy\\:\\:__construct\\(\\) has parameter \\$couponRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\PromotionCouponRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Component/Promotion/Factory/PromotionCouponFactoryInterface.php + path: src/Sylius/Component/Promotion/Generator/PercentageGenerationPolicy.php - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Generator\\\\PromotionCouponGenerator\\:\\:__construct\\(\\) has parameter \\$couponFactory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Component/Promotion/Generator/PromotionCouponGenerator.php + - + message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Generator\\\\PromotionCouponGenerator\\:\\:__construct\\(\\) has parameter \\$couponRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\PromotionCouponRepositoryInterface but does not specify its types\\: T$#" + count: 1 + path: src/Sylius/Component/Promotion/Generator/PromotionCouponGenerator.php + - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Generator\\\\PromotionCouponGenerator\\:\\:generateUniqueCode\\(\\) has parameter \\$generatedCoupons with no value type specified in iterable type array\\.$#" count: 1 @@ -6150,26 +6630,11 @@ parameters: count: 1 path: src/Sylius/Component/Promotion/Generator/PromotionCouponGenerator.php - - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\CatalogPromotion\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Promotion/Model/CatalogPromotion.php - - - - message: "#^Property Sylius\\\\Component\\\\Promotion\\\\Model\\\\CatalogPromotion\\:\\:\\$actions with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#" - count: 1 - path: src/Sylius/Component/Promotion/Model/CatalogPromotion.php - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\CatalogPromotionAction\\:\\:getConfiguration\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Promotion/Model/CatalogPromotionAction.php - - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\CatalogPromotionAction\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Promotion/Model/CatalogPromotionAction.php - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\CatalogPromotionAction\\:\\:setConfiguration\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -6195,11 +6660,6 @@ parameters: count: 1 path: src/Sylius/Component/Promotion/Model/CatalogPromotionScope.php - - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\CatalogPromotionScope\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Promotion/Model/CatalogPromotionScope.php - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\CatalogPromotionScope\\:\\:setConfiguration\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -6220,31 +6680,16 @@ parameters: count: 1 path: src/Sylius/Component/Promotion/Model/CatalogPromotionScopeInterface.php - - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\CatalogPromotionTranslation\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Promotion/Model/CatalogPromotionTranslation.php - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\ConfigurablePromotionElementInterface\\:\\:getConfiguration\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Promotion/Model/ConfigurablePromotionElementInterface.php - - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\Promotion\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Promotion/Model/Promotion.php - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\PromotionAction\\:\\:getConfiguration\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Promotion/Model/PromotionAction.php - - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\PromotionAction\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Promotion/Model/PromotionAction.php - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\PromotionAction\\:\\:setConfiguration\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -6255,21 +6700,11 @@ parameters: count: 1 path: src/Sylius/Component/Promotion/Model/PromotionActionInterface.php - - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\PromotionCoupon\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Promotion/Model/PromotionCoupon.php - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\PromotionRule\\:\\:getConfiguration\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Promotion/Model/PromotionRule.php - - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\PromotionRule\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Promotion/Model/PromotionRule.php - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Model\\\\PromotionRule\\:\\:setConfiguration\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -6281,20 +6716,15 @@ parameters: path: src/Sylius/Component/Promotion/Model/PromotionRuleInterface.php - - message: "#^Interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\CatalogPromotionRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" + message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Provider\\\\ActivePromotionsProvider\\:\\:__construct\\(\\) has parameter \\$promotionRepository with generic interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\PromotionRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Component/Promotion/Repository/CatalogPromotionRepositoryInterface.php + path: src/Sylius/Component/Promotion/Provider/ActivePromotionsProvider.php - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Repository\\\\CatalogPromotionRepositoryInterface\\:\\:findByCriteria\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Promotion/Repository/CatalogPromotionRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\PromotionCouponRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Promotion/Repository/PromotionCouponRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Promotion\\\\Repository\\\\PromotionCouponRepositoryInterface\\:\\:createPaginatorForPromotion\\(\\) return type has no value type specified in iterable type iterable\\.$#" count: 1 @@ -6305,26 +6735,11 @@ parameters: count: 1 path: src/Sylius/Component/Promotion/Repository/PromotionCouponRepositoryInterface.php - - - message: "#^Interface Sylius\\\\Component\\\\Promotion\\\\Repository\\\\PromotionRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Promotion/Repository/PromotionRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Review\\\\Factory\\\\ReviewFactory\\:\\:__construct\\(\\) has parameter \\$factory with generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Component/Review/Factory/ReviewFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Review\\\\Factory\\\\ReviewFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Review/Factory/ReviewFactoryInterface.php - - - - message: "#^Method Sylius\\\\Component\\\\Review\\\\Model\\\\Review\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Review/Model/Review.php - - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Calculator\\\\CalculatorInterface\\:\\:calculate\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -6360,31 +6775,11 @@ parameters: count: 1 path: src/Sylius/Component/Shipping/Model/ConfigurableShippingMethodElementInterface.php - - - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Model\\\\Shipment\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Shipping/Model/Shipment.php - - - - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Model\\\\ShipmentUnit\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Shipping/Model/ShipmentUnit.php - - - - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Model\\\\ShippingCategory\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Shipping/Model/ShippingCategory.php - - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Model\\\\ShippingMethod\\:\\:getConfiguration\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 path: src/Sylius/Component/Shipping/Model/ShippingMethod.php - - - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Model\\\\ShippingMethod\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Shipping/Model/ShippingMethod.php - - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Model\\\\ShippingMethod\\:\\:setConfiguration\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -6410,11 +6805,6 @@ parameters: count: 1 path: src/Sylius/Component/Shipping/Model/ShippingMethodRule.php - - - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Model\\\\ShippingMethodRule\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Shipping/Model/ShippingMethodRule.php - - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Model\\\\ShippingMethodRule\\:\\:setConfiguration\\(\\) has parameter \\$configuration with no value type specified in iterable type array\\.$#" count: 1 @@ -6426,35 +6816,15 @@ parameters: path: src/Sylius/Component/Shipping/Model/ShippingMethodRuleInterface.php - - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Model\\\\ShippingMethodTranslation\\:\\:getId\\(\\) has no return type specified\\.$#" + message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Resolver\\\\DefaultShippingMethodResolver\\:\\:__construct\\(\\) has parameter \\$shippingMethodRepository with generic interface Sylius\\\\Component\\\\Shipping\\\\Repository\\\\ShippingMethodRepositoryInterface but does not specify its types\\: T$#" count: 1 - path: src/Sylius/Component/Shipping/Model/ShippingMethodTranslation.php - - - - message: "#^Interface Sylius\\\\Component\\\\Shipping\\\\Repository\\\\ShippingMethodRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Shipping/Repository/ShippingMethodRepositoryInterface.php + path: src/Sylius/Component/Shipping/Resolver/DefaultShippingMethodResolver.php - message: "#^Method Sylius\\\\Component\\\\Shipping\\\\Resolver\\\\ShippingMethodsResolver\\:\\:__construct\\(\\) has parameter \\$shippingMethodRepository with generic interface Doctrine\\\\Persistence\\\\ObjectRepository but does not specify its types\\: TEntityClass$#" count: 1 path: src/Sylius/Component/Shipping/Resolver/ShippingMethodsResolver.php - - - message: "#^Method Sylius\\\\Component\\\\Taxation\\\\Model\\\\TaxCategory\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Taxation/Model/TaxCategory.php - - - - message: "#^Method Sylius\\\\Component\\\\Taxation\\\\Model\\\\TaxRate\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Taxation/Model/TaxRate.php - - - - message: "#^Interface Sylius\\\\Component\\\\Taxation\\\\Repository\\\\TaxCategoryRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Taxation/Repository/TaxCategoryRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Taxation\\\\Resolver\\\\TaxRateResolver\\:\\:__construct\\(\\) has parameter \\$taxRateRepository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 @@ -6480,26 +6850,6 @@ parameters: count: 1 path: src/Sylius/Component/Taxonomy/Factory/TaxonFactory.php - - - message: "#^Interface Sylius\\\\Component\\\\Taxonomy\\\\Factory\\\\TaxonFactoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Factory\\\\FactoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Taxonomy/Factory/TaxonFactoryInterface.php - - - - message: "#^Method Sylius\\\\Component\\\\Taxonomy\\\\Model\\\\Taxon\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Taxonomy/Model/Taxon.php - - - - message: "#^Method Sylius\\\\Component\\\\Taxonomy\\\\Model\\\\TaxonTranslation\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/Taxonomy/Model/TaxonTranslation.php - - - - message: "#^Interface Sylius\\\\Component\\\\Taxonomy\\\\Repository\\\\TaxonRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/Taxonomy/Repository/TaxonRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\Taxonomy\\\\Repository\\\\TaxonRepositoryInterface\\:\\:findChildrenByChannelMenuTaxon\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -6512,7 +6862,7 @@ parameters: - message: "#^Method Sylius\\\\Component\\\\User\\\\Model\\\\CredentialsHolderInterface\\:\\:eraseCredentials\\(\\) has no return type specified\\.$#" - count: 1 + count: 2 path: src/Sylius/Component/User/Model/CredentialsHolderInterface.php - @@ -6520,11 +6870,6 @@ parameters: count: 1 path: src/Sylius/Component/User/Model/User.php - - - message: "#^Method Sylius\\\\Component\\\\User\\\\Model\\\\User\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/User/Model/User.php - - message: "#^Method Sylius\\\\Component\\\\User\\\\Model\\\\User\\:\\:getRoles\\(\\) return type has no value type specified in iterable type array\\.$#" count: 1 @@ -6540,17 +6885,97 @@ parameters: count: 1 path: src/Sylius/Component/User/Model/UserInterface.php - - - message: "#^Method Sylius\\\\Component\\\\User\\\\Model\\\\UserOAuth\\:\\:getId\\(\\) has no return type specified\\.$#" - count: 1 - path: src/Sylius/Component/User/Model/UserOAuth.php - - - - message: "#^Interface Sylius\\\\Component\\\\User\\\\Repository\\\\UserRepositoryInterface extends generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" - count: 1 - path: src/Sylius/Component/User/Repository/UserRepositoryInterface.php - - message: "#^Method Sylius\\\\Component\\\\User\\\\Security\\\\Checker\\\\TokenUniquenessChecker\\:\\:__construct\\(\\) has parameter \\$repository with generic interface Sylius\\\\Component\\\\Resource\\\\Repository\\\\RepositoryInterface but does not specify its types\\: T$#" count: 1 path: src/Sylius/Component/User/Security/Checker/TokenUniquenessChecker.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/CancelUnpaidOrdersCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/CheckRequirementsCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/InformAboutGUSCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/InstallAssetsCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/InstallCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/InstallDatabaseCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/InstallSampleDataCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/JwtConfigurationCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/SetupCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/ShowAvailablePluginsCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/CoreBundle/Command/Model/PluginInfo.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/OrderBundle/Command/RemoveExpiredCartsCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/UiBundle/Command/DebugTemplateEventCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/UserBundle/Command/AbstractRoleCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/UserBundle/Command/DemoteUserCommand.php + + - + message: "#^If condition is always false.$#" + count: 1 + path: src/Sylius/Bundle/UserBundle/Command/PromoteUserCommand.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 9fd9a45ff9..6deadb25a2 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -35,7 +35,11 @@ parameters: - 'src/Sylius/Bundle/CoreBundle/Doctrine/DQL/**.php' - 'src/Sylius/Bundle/CoreBundle/Doctrine/ORM/SqlWalker/**.php' ignoreErrors: + - '/(Interface|Class) [a-zA-Z\\]+ specifies template type (\w+) of interface [a-zA-Z\\]+ as [a-zA-Z\\]+ (of [a-zA-Z\\]+ )?but it''s already specified as/' # turns off a weird generics check behavior, we are basing on Psalm for generics checks - '/Access to an undefined property Doctrine\\Common\\Collections\\ArrayCollection/' + - '/Class "Sylius\\Bundle\\CoreBundle\\Fixture\\PaymentFixture" not found/' + - '/Class Symfony\\Component\\Clock\\Clock not found\./' - '/Class Symfony\\Component\\Security\\Core\\Authentication\\Token\\UsernamePasswordToken constructor invoked with 4 parameters\, 2\-3 required./' - '/Method Symfony\\Component\\Serializer\\Normalizer\\NormalizerInterface\:\:supportsNormalization\(\) invoked with 3 parameters\, 1\-2 required\./' - '/Symfony\\Component\\Serializer\\NameConverter\\NameConverterInterface::normalize\(\) invoked with 2 parameters, 1 required./' + - '/Method Sylius\\Component\\(\w+)\\Model\\(\w+)\:\:getId\(\) has no return type specified./' diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f7c4b7da20..9390adf949 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -24,5 +24,7 @@ + + diff --git a/src/Sylius/Abstraction/StateMachine/.gitignore b/src/Sylius/Abstraction/StateMachine/.gitignore new file mode 100644 index 0000000000..4901aabd96 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/.gitignore @@ -0,0 +1,3 @@ +/vendor +/composer.lock +/.phpunit.result.cache diff --git a/src/Sylius/Abstraction/StateMachine/LICENSE b/src/Sylius/Abstraction/StateMachine/LICENSE new file mode 100644 index 0000000000..f86a6d1ff4 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024 Sylius Sp. z o.o. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Sylius/Abstraction/StateMachine/README.md b/src/Sylius/Abstraction/StateMachine/README.md new file mode 100644 index 0000000000..95af06714c --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/README.md @@ -0,0 +1,55 @@ +StateMachineAbstraction +======================= + +State Machine Abstraction package provides a configurable abstraction for Sylius that allows you to define which adapter +should be used (Winzou State Machine or Symfony Workflow) per graph. + +Sylius +------ + +![Sylius](https://demo.sylius.com/assets/shop/img/logo.png) + +Sylius is an Open Source eCommerce solution built from decoupled components with powerful API and the highest quality code. [Read more on sylius.com](https://sylius.com). + +Documentation +------------- + +Documentation is available on [**docs.sylius.com**](https://docs.sylius.com). + +Contributing +------------ + +[This page](https://docs.sylius.com/en/latest/contributing/index.html) contains all the information about contributing to Sylius. + +Follow Sylius' Development +-------------------------- + +If you want to keep up with the updates and latest features, follow us on the following channels: + +* [Official Blog](https://sylius.com/blog) +* [Sylius on Twitter](https://twitter.com/Sylius) +* [Sylius on Facebook](https://facebook.com/SyliusEcommerce) + +Bug tracking +------------ + +Sylius uses [GitHub issues](https://github.com/Sylius/Sylius/issues). +If you have found bug, please create an issue. + +MIT License +----------- + +License can be found [here](https://github.com/Sylius/Sylius/blob/master/LICENSE). + +Authors +------- + +See the list of [contributors](https://github.com/Sylius/Sylius/contributors). + +Testing +----------------------- + +To run tests: +```bash +(cd src/Sylius/Abstraction/StateMachine && vendor/bin/phpunit) +``` diff --git a/src/Sylius/Abstraction/StateMachine/composer.json b/src/Sylius/Abstraction/StateMachine/composer.json new file mode 100644 index 0000000000..b0817b2060 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/composer.json @@ -0,0 +1,43 @@ +{ + "name": "sylius/state-machine-abstraction", + "description": "Abstraction layer for State Machine used in Sylius bundles", + "type": "symfony-bundle", + "license": "MIT", + "authors": [ + { + "name": "Jacob Tobiasz", + "email": "jakub.tobiasz@icloud.com" + } + ], + "autoload": { + "psr-4": { + "Sylius\\Abstraction\\StateMachine\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\Sylius\\Abstraction\\StateMachine\\": "tests/" + } + }, + "require": { + "php": "^8.1", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/http-kernel": "^5.4.21 || ^6.4", + "symfony/workflow": "^5.4.21 || ^6.4", + "winzou/state-machine": "^0.4", + "winzou/state-machine-bundle": "^0.6" + }, + "require-dev": { + "matthiasnoback/symfony-config-test": "^4.2", + "phpunit/phpunit": "^9.5", + "webmozart/assert": "^1.9" + }, + "extra": { + "branch-alias": { + "dev-main": "1.13-dev" + }, + "symfony": { + "require": "^5.4.21" + } + } +} diff --git a/src/Sylius/Abstraction/StateMachine/config/services.xml b/src/Sylius/Abstraction/StateMachine/config/services.xml new file mode 100644 index 0000000000..8ec1b9840e --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/config/services.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + %sylius_abstraction.state_machine.default_adapter% + %sylius_abstraction.state_machine.graphs_to_adapters_mapping% + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Abstraction/StateMachine/phpunit.xml.dist b/src/Sylius/Abstraction/StateMachine/phpunit.xml.dist new file mode 100644 index 0000000000..08e1357ca9 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/phpunit.xml.dist @@ -0,0 +1,18 @@ + + + + + + + tests + + + + + + + diff --git a/src/Sylius/Abstraction/StateMachine/src/CompositeStateMachine.php b/src/Sylius/Abstraction/StateMachine/src/CompositeStateMachine.php new file mode 100644 index 0000000000..ad7013e235 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/src/CompositeStateMachine.php @@ -0,0 +1,81 @@ + */ + private array $stateMachineAdapters; + + /** + * @param iterable $stateMachineAdapters + * @param array $graphsToAdaptersMapping + */ + public function __construct( + iterable $stateMachineAdapters, + private string $defaultAdapter, + private array $graphsToAdaptersMapping, + ) { + Assert::notEmpty($stateMachineAdapters, 'At least one state machine adapter should be provided.'); + Assert::allIsInstanceOf( + $stateMachineAdapters, + StateMachineInterface::class, + sprintf('All state machine adapters should implement the "%s" interface.', StateMachineInterface::class), + ); + $this->stateMachineAdapters = $stateMachineAdapters instanceof Traversable ? iterator_to_array($stateMachineAdapters) : $stateMachineAdapters; + } + + /** @throws StateMachineExecutionException */ + public function can(object $subject, string $graphName, string $transition): bool + { + return $this->getStateMachineAdapter($graphName)->can($subject, $graphName, $transition); + } + + /** @throws StateMachineExecutionException */ + public function apply(object $subject, string $graphName, string $transition, array $context = []): void + { + $this->getStateMachineAdapter($graphName)->apply($subject, $graphName, $transition, $context); + } + + /** @throws StateMachineExecutionException */ + public function getEnabledTransitions(object $subject, string $graphName): array + { + return $this->getStateMachineAdapter($graphName)->getEnabledTransitions($subject, $graphName); + } + + /** @throws StateMachineExecutionException */ + public function getTransitionFromState(object $subject, string $graphName, string $fromState): ?string + { + return $this->getStateMachineAdapter($graphName)->getTransitionFromState($subject, $graphName, $fromState); + } + + /** @throws StateMachineExecutionException */ + public function getTransitionToState(object $subject, string $graphName, string $toState): ?string + { + return $this->getStateMachineAdapter($graphName)->getTransitionToState($subject, $graphName, $toState); + } + + private function getStateMachineAdapter(string $graphName): StateMachineInterface + { + if (isset($this->graphsToAdaptersMapping[$graphName])) { + return $this->stateMachineAdapters[$this->graphsToAdaptersMapping[$graphName]]; + } + + return $this->stateMachineAdapters[$this->defaultAdapter]; + } +} diff --git a/src/Sylius/Abstraction/StateMachine/src/DependencyInjection/Configuration.php b/src/Sylius/Abstraction/StateMachine/src/DependencyInjection/Configuration.php new file mode 100644 index 0000000000..57d7088fbd --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/src/DependencyInjection/Configuration.php @@ -0,0 +1,41 @@ +getRootNode(); + + $rootNode + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('default_adapter')->defaultValue('winzou_state_machine')->end() + ->arrayNode('graphs_to_adapters_mapping') + ->useAttributeAsKey('graph_name') + ->scalarPrototype()->end() + ->end() + ->end() + ; + + return $treeBuilder; + } +} diff --git a/src/Sylius/Abstraction/StateMachine/src/DependencyInjection/SyliusStateMachineAbstractionExtension.php b/src/Sylius/Abstraction/StateMachine/src/DependencyInjection/SyliusStateMachineAbstractionExtension.php new file mode 100644 index 0000000000..d21a765168 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/src/DependencyInjection/SyliusStateMachineAbstractionExtension.php @@ -0,0 +1,33 @@ +processConfiguration($this->getConfiguration([], $container), $configs); + + $loader = new XmlFileLoader($container, new FileLocator(dirname(__DIR__, 2) . '/config/')); + $loader->load('services.xml'); + + $container->setParameter('sylius_abstraction.state_machine.default_adapter', $config['default_adapter']); + $container->setParameter('sylius_abstraction.state_machine.graphs_to_adapters_mapping', $config['graphs_to_adapters_mapping']); + } +} diff --git a/src/Sylius/Abstraction/StateMachine/src/Exception/StateMachineExecutionException.php b/src/Sylius/Abstraction/StateMachine/src/Exception/StateMachineExecutionException.php new file mode 100644 index 0000000000..0e183e1d26 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/src/Exception/StateMachineExecutionException.php @@ -0,0 +1,18 @@ + $context + * + * @throws StateMachineExecutionException + */ + public function apply(object $subject, string $graphName, string $transition, array $context = []): void; + + /** + * @throws StateMachineExecutionException + * + * @return array + */ + public function getEnabledTransitions(object $subject, string $graphName): array; + + /** + * @throws StateMachineExecutionException + */ + public function getTransitionFromState(object $subject, string $graphName, string $fromState): ?string; + + /** + * @throws StateMachineExecutionException + */ + public function getTransitionToState(object $subject, string $graphName, string $toState): ?string; +} diff --git a/src/Sylius/Abstraction/StateMachine/src/SyliusStateMachineAbstractionBundle.php b/src/Sylius/Abstraction/StateMachine/src/SyliusStateMachineAbstractionBundle.php new file mode 100644 index 0000000000..6874d546e6 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/src/SyliusStateMachineAbstractionBundle.php @@ -0,0 +1,24 @@ +symfonyWorkflowRegistry->get($subject, $graphName)->can($subject, $transition); + } catch (WorkflowExceptionInterface $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + } + + public function apply(object $subject, string $graphName, string $transition, array $context = []): void + { + try { + $this->symfonyWorkflowRegistry->get($subject, $graphName)->apply($subject, $transition, $context); + } catch (WorkflowExceptionInterface $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + } + + public function getEnabledTransitions(object $subject, string $graphName): array + { + try { + $enabledTransitions = $this->symfonyWorkflowRegistry->get($subject, $graphName)->getEnabledTransitions($subject); + } catch (WorkflowExceptionInterface $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + + return array_map( + function (SymfonyWorkflowTransition $transition): TransitionInterface { + return new Transition( + $transition->getName(), + $transition->getFroms(), + $transition->getTos(), + ); + }, + $enabledTransitions, + ); + } + + public function getTransitionFromState(object $subject, string $graphName, string $fromState): ?string + { + foreach ($this->getEnabledTransitions($subject, $graphName) as $transition) { + if ($transition->getFroms() !== null && in_array($fromState, $transition->getFroms(), true)) { + return $transition->getName(); + } + } + + return null; + } + + public function getTransitionToState(object $subject, string $graphName, string $toState): ?string + { + foreach ($this->getEnabledTransitions($subject, $graphName) as $transition) { + if ($transition->getTos() !== null && in_array($toState, $transition->getTos(), true)) { + return $transition->getName(); + } + } + + return null; + } +} diff --git a/src/Sylius/Abstraction/StateMachine/src/Transition.php b/src/Sylius/Abstraction/StateMachine/src/Transition.php new file mode 100644 index 0000000000..63d006ebaf --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/src/Transition.php @@ -0,0 +1,43 @@ +|null $froms + * @param array|null $tos + */ + public function __construct( + private string $name, + private ?array $froms, + private ?array $tos, + ) { + } + + public function getName(): string + { + return $this->name; + } + + public function getFroms(): ?array + { + return $this->froms; + } + + public function getTos(): ?array + { + return $this->tos; + } +} diff --git a/src/Sylius/Abstraction/StateMachine/src/TransitionInterface.php b/src/Sylius/Abstraction/StateMachine/src/TransitionInterface.php new file mode 100644 index 0000000000..b62c217f93 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/src/TransitionInterface.php @@ -0,0 +1,29 @@ +|null + */ + public function getFroms(): ?array; + + /** + * @return array|null + */ + public function getTos(): ?array; +} diff --git a/src/Sylius/Abstraction/StateMachine/src/WinzouStateMachineAdapter.php b/src/Sylius/Abstraction/StateMachine/src/WinzouStateMachineAdapter.php new file mode 100644 index 0000000000..c6eb6a8d31 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/src/WinzouStateMachineAdapter.php @@ -0,0 +1,120 @@ +getStateMachine($subject, $graphName)->can($transition); + } catch (SMException $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + } + + public function apply(object $subject, string $graphName, string $transition, array $context = []): void + { + try { + $this->getStateMachine($subject, $graphName)->apply($transition); + } catch (SMException $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + } + + public function getEnabledTransitions(object $subject, string $graphName): array + { + $stateMachine = $this->getStateMachine($subject, $graphName); + + return array_filter( + $this->getAllTransitions($stateMachine), + fn (TransitionInterface $transition) => $this->can($subject, $graphName, $transition->getName()), + ); + } + + /** + * @return array + */ + private function getAllTransitions(\SM\StateMachine\StateMachineInterface $stateMachine): array + { + try { + $transitionsConfig = $this->getConfig($stateMachine)['transitions']; + } catch (\ReflectionException $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + + $transitions = []; + + foreach ($transitionsConfig as $transitionName => $transitionConfig) { + $froms = $transitionConfig['from']; + $tos = [$transitionConfig['to']]; + $transitions[] = new Transition($transitionName, $froms, $tos); + } + + return $transitions; + } + + /** + * @throws \ReflectionException + * + * @return array{transitions: array, to: string}>} + */ + private function getConfig(\SM\StateMachine\StateMachineInterface $stateMachine): array + { + $reflection = new \ReflectionClass($stateMachine); + $configProperty = $reflection->getProperty('config'); + $configProperty->setAccessible(true); + + return $configProperty->getValue($stateMachine); + } + + public function getTransitionFromState(object $subject, string $graphName, string $fromState): ?string + { + foreach ($this->getEnabledTransitions($subject, $graphName) as $transition) { + if ($transition->getFroms() !== null && in_array($fromState, $transition->getFroms(), true)) { + return $transition->getName(); + } + } + + return null; + } + + public function getTransitionToState(object $subject, string $graphName, string $toState): ?string + { + foreach ($this->getEnabledTransitions($subject, $graphName) as $transition) { + if ($transition->getTos() !== null && in_array($toState, $transition->getTos(), true)) { + return $transition->getName(); + } + } + + return null; + } + + private function getStateMachine(object $subject, string $graphName): \SM\StateMachine\StateMachineInterface + { + try { + return $this->winzouStateMachineFactory->get($subject, $graphName); + } catch (SMException $exception) { + throw new StateMachineExecutionException($exception->getMessage(), $exception->getCode(), $exception); + } + } +} diff --git a/src/Sylius/Abstraction/StateMachine/tests/Functional/DependencyInjection/ConfigurationTest.php b/src/Sylius/Abstraction/StateMachine/tests/Functional/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000000..c92a412b76 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/tests/Functional/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,67 @@ +assertProcessedConfigurationEquals( + [ + [ + 'default_adapter' => 'symfony_workflow', + ], + ], + [ + 'default_adapter' => 'symfony_workflow', + 'graphs_to_adapters_mapping' => [], + ], + ); + } + + /** @test */ + public function it_allows_to_configure_the_state_machines_adapters_mapping(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'graphs_to_adapters_mapping' => [ + 'order' => 'symfony_workflow', + 'payment' => 'winzou_state_machine', + ], + ], + ], + [ + 'default_adapter' => 'winzou_state_machine', + 'graphs_to_adapters_mapping' => [ + 'order' => 'symfony_workflow', + 'payment' => 'winzou_state_machine', + ], + ], + ); + } + + protected function getConfiguration(): ConfigurationInterface + { + return new Configuration(); + } +} diff --git a/src/Sylius/Abstraction/StateMachine/tests/Unit/CompositeStateMachineTest.php b/src/Sylius/Abstraction/StateMachine/tests/Unit/CompositeStateMachineTest.php new file mode 100644 index 0000000000..861da446d9 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/tests/Unit/CompositeStateMachineTest.php @@ -0,0 +1,107 @@ +someStateMachineAdapter = $this->createMock(StateMachineInterface::class); + $this->anotherStateMachineAdapter = $this->createMock(StateMachineInterface::class); + } + + public function testItThrowsAnExceptionIfNoStateMachineAdapterIsProvided(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('At least one state machine adapter should be provided.'); + + $this->createTestSubject(stateMachineAdapters: []); + } + + public function testItThrowsAnExceptionIfStateMachineAdapterDoesNotImplementTheInterface(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('All state machine adapters should implement the "Sylius\Abstraction\StateMachine\StateMachineInterface" interface.'); + + $this->createTestSubject(stateMachineAdapters: [$this->createMock(\stdClass::class)]); + } + + public function testItUsesMappedAdapterToUseWhetherTransitionCanBeApplied(): void + { + $this->someStateMachineAdapter->expects($this->never())->method('can'); + $this->anotherStateMachineAdapter->expects($this->once())->method('can')->willReturn(true); + + $stateMachine = $this->createTestSubject(); + $this->assertTrue($stateMachine->can(new \stdClass(), 'another_graph', 'some_transition')); + } + + public function testItAppliesTransitionUsingMappedAdapter(): void + { + $this->someStateMachineAdapter->expects($this->once())->method('apply'); + $this->anotherStateMachineAdapter->expects($this->never())->method('apply'); + + $stateMachine = $this->createTestSubject(); + $stateMachine->apply(new \stdClass(), 'some_graph', 'some_transition'); + } + + public function testItReturnsEnabledTransitionsUsingMappedAdapter(): void + { + $this->someStateMachineAdapter->expects($this->never())->method('getEnabledTransitions'); + $this->anotherStateMachineAdapter->expects($this->once())->method('getEnabledTransitions')->willReturn(['some_transition']); + + $stateMachine = $this->createTestSubject(); + $this->assertSame(['some_transition'], $stateMachine->getEnabledTransitions(new \stdClass(), 'another_graph')); + } + + public function testItReturnsTransitionFromStateUsingMappedAdapter(): void + { + $this->someStateMachineAdapter->expects($this->never())->method('getTransitionFromState'); + $this->anotherStateMachineAdapter->expects($this->once())->method('getTransitionFromState')->willReturn('some_transition'); + + $stateMachine = $this->createTestSubject(); + $this->assertSame('some_transition', $stateMachine->getTransitionFromState(new \stdClass(), 'another_graph', 'some_state')); + } + + public function testItReturnsTransitionToStateUsingMappedAdapter(): void + { + $this->someStateMachineAdapter->expects($this->never())->method('getTransitionToState'); + $this->anotherStateMachineAdapter->expects($this->once())->method('getTransitionToState')->willReturn('some_transition'); + + $stateMachine = $this->createTestSubject(); + $this->assertSame('some_transition', $stateMachine->getTransitionToState(new \stdClass(), 'another_graph', 'some_state')); + } + + private function createTestSubject(mixed ...$arguments): StateMachineInterface + { + return new CompositeStateMachine(...array_replace([ + 'stateMachineAdapters' => ['some_adapter' => $this->someStateMachineAdapter, 'another_adapter' => $this->anotherStateMachineAdapter], + 'defaultAdapter' => 'some_adapter', + 'graphsToAdaptersMapping' => ['some_graph' => 'some_adapter', 'another_graph' => 'another_adapter'], + ], $arguments)); + } +} diff --git a/src/Sylius/Abstraction/StateMachine/tests/Unit/SymfonyWorkflowAdapterTest.php b/src/Sylius/Abstraction/StateMachine/tests/Unit/SymfonyWorkflowAdapterTest.php new file mode 100644 index 0000000000..c60c83ab70 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/tests/Unit/SymfonyWorkflowAdapterTest.php @@ -0,0 +1,274 @@ +symfonyWorkflowRegistry = $this->createMock(Registry::class); + } + + public function testItReturnWhetherTransitionCanBeApplied(): void + { + $subject = new \stdClass(); + $graphName = 'graph_name'; + $transition = 'transition'; + + $workflow = $this->createMock(Workflow::class); + $workflow + ->expects($this->once()) + ->method('can') + ->with($subject, $transition) + ->willReturn(true) + ; + + $this->symfonyWorkflowRegistry + ->expects($this->once()) + ->method('get') + ->with($subject, $graphName) + ->willReturn($workflow) + ; + + $this->createTestSubject()->can($subject, $graphName, $transition); + } + + public function testItAppliesTransition(): void + { + $subject = new \stdClass(); + $graphName = 'graph_name'; + $transition = 'transition'; + /** @var array $context */ + $context = ['context']; + + $workflow = $this->createMock(Workflow::class); + $workflow + ->expects($this->once()) + ->method('apply') + ->with($subject, $transition, $context) + ; + + $this->symfonyWorkflowRegistry + ->expects($this->once()) + ->method('get') + ->with($subject, $graphName) + ->willReturn($workflow) + ; + + $this->createTestSubject()->apply($subject, $graphName, $transition, $context); + } + + public function testItReturnsEnabledTransitions(): void + { + $subject = new \stdClass(); + $graphName = 'graph_name'; + + $workflow = $this->createMock(Workflow::class); + $workflow + ->expects($this->once()) + ->method('getEnabledTransitions') + ->with($subject) + ->willReturn([$this->createSampleTransition()]) + ; + + $this->symfonyWorkflowRegistry + ->expects($this->once()) + ->method('get') + ->with($subject, $graphName) + ->willReturn($workflow) + ; + + $enabledTransition = $this->createTestSubject()->getEnabledTransitions($subject, $graphName); + + $this->assertCount(1, $enabledTransition); + $this->assertSame('transition', $enabledTransition[0]->getName()); + $this->assertSame(['from'], $enabledTransition[0]->getFroms()); + $this->assertSame(['to'], $enabledTransition[0]->getTos()); + } + + public function testItConvertsWorkflowExceptionsToCustomOnesOnCan(): void + { + $this->expectException(StateMachineExecutionException::class); + + $subject = new \stdClass(); + $graphName = 'graph_name'; + $transition = 'transition'; + + $this->symfonyWorkflowRegistry->method('get')->willThrowException(new InvalidArgumentException()); + + $this->createTestSubject()->can($subject, $graphName, $transition); + } + + public function testItConvertsWorkflowExceptionsToCustomOnApply(): void + { + $this->expectException(StateMachineExecutionException::class); + + $subject = new \stdClass(); + $graphName = 'graph_name'; + $transition = 'transition'; + + $this->symfonyWorkflowRegistry->method('get')->willThrowException(new InvalidArgumentException()); + + $this->createTestSubject()->apply($subject, $graphName, $transition); + } + + public function testItConvertsWorkflowExceptionsToCustomOnGetEnabledTransitions(): void + { + $this->expectException(StateMachineExecutionException::class); + + $subject = new \stdClass(); + $graphName = 'graph_name'; + + $this->symfonyWorkflowRegistry->method('get')->willThrowException(new InvalidArgumentException()); + + $this->createTestSubject()->getEnabledTransitions($subject, $graphName); + } + + /** + * @dataProvider itReturnsTransitionsToForGivenTransitionProvider + */ + public function testItReturnsTransitionsToForGivenTransition(?string $expectedToStateTransition, string $requestedTransition): void + { + $subject = new \stdClass(); + $graphName = 'graph_name'; + + $someTransition = $this->createSampleTransition( + name: 'some_transition', + froms: ['from_1', 'from_2'], + tos: ['to_1'], + ); + $anotherTransition = $this->createSampleTransition( + name: 'another_transition', + froms: ['from_1', 'from_3'], + tos: ['to_1', 'to_2'], + ); + + $workflow = $this->createMock(Workflow::class); + $workflow->method('getEnabledTransitions')->with($subject)->willReturn([$someTransition, $anotherTransition]); + + $this->symfonyWorkflowRegistry->method('get')->with($subject, $graphName)->willReturn($workflow); + + $testSubject = $this->createTestSubject(); + + $this->assertSame( + $expectedToStateTransition, + $testSubject->getTransitionToState($subject, $graphName, $requestedTransition), + ); + } + + /** + * @return iterable + */ + public static function itReturnsTransitionsToForGivenTransitionProvider(): iterable + { + yield 'it returns first transition for to_1' => [ + 'expectedToStateTransition' => 'some_transition', + 'requestedTransition' => 'to_1', + ]; + yield ' it returns second transition for to_2' => [ + 'expectedToStateTransition' => 'another_transition', + 'requestedTransition' => 'to_2', + ]; + yield 'it returns null for to_3' => [ + 'expectedToStateTransition' => null, + 'requestedTransition' => 'to_3', + ]; + } + + /** + * @dataProvider itReturnsTransitionsFromForGivenTransitionProvider + */ + public function testItReturnsTransitionsFromForGivenTransition(?string $expectedFromStateTransition, string $requestedTransition): void + { + $subject = new \stdClass(); + $graphName = 'graph_name'; + + $someTransition = $this->createSampleTransition( + name: 'some_transition', + froms: ['from_1', 'from_2'], + tos: ['to_1'], + ); + $anotherTransition = $this->createSampleTransition( + name: 'another_transition', + froms: ['from_1', 'from_3'], + tos: ['to_1', 'to_2'], + ); + + $workflow = $this->createMock(Workflow::class); + $workflow->method('getEnabledTransitions')->with($subject)->willReturn([$someTransition, $anotherTransition]); + + $this->symfonyWorkflowRegistry->method('get')->with($subject, $graphName)->willReturn($workflow); + + $testSubject = $this->createTestSubject(); + + $this->assertSame( + $expectedFromStateTransition, + $testSubject->getTransitionFromState($subject, $graphName, $requestedTransition), + ); + } + + /** + * @return iterable + */ + public static function itReturnsTransitionsFromForGivenTransitionProvider(): iterable + { + yield 'it returns first transition for from_1' => [ + 'expectedFromStateTransition' => 'some_transition', + 'requestedTransition' => 'from_1', + ]; + yield 'it returns first transition for from_2' => [ + 'expectedFromStateTransition' => 'some_transition', + 'requestedTransition' => 'from_2', + ]; + yield 'it returns second transition for from_3' => [ + 'expectedFromStateTransition' => 'another_transition', + 'requestedTransition' => 'from_3', + ]; + yield 'it returns null for from_4' => [ + 'expectedFromStateTransition' => null, + 'requestedTransition' => 'from_4', + ]; + } + + private function createTestSubject(): SymfonyWorkflowAdapter + { + return new SymfonyWorkflowAdapter($this->symfonyWorkflowRegistry); + } + + private function createSampleTransition(mixed ...$arguments): SymfonyWorkflowTransition + { + return new SymfonyWorkflowTransition(...array_replace( + [ + 'name' => 'transition', + 'froms' => ['from'], + 'tos' => ['to'], + ], + $arguments, + )); + } +} diff --git a/src/Sylius/Abstraction/StateMachine/tests/Unit/TransitionTest.php b/src/Sylius/Abstraction/StateMachine/tests/Unit/TransitionTest.php new file mode 100644 index 0000000000..b5ac8a028c --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/tests/Unit/TransitionTest.php @@ -0,0 +1,40 @@ +assertSame('name', $this->createTestSubject()->getName()); + } + + public function testItReturnsItsFroms(): void + { + $this->assertSame(['from'], $this->createTestSubject()->getFroms()); + } + + public function testItReturnsItsTos(): void + { + $this->assertSame(['to'], $this->createTestSubject()->getTos()); + } + + private function createTestSubject(): Transition + { + return new Transition('name', ['from'], ['to']); + } +} diff --git a/src/Sylius/Abstraction/StateMachine/tests/Unit/WinzouStateMachineAdapterTest.php b/src/Sylius/Abstraction/StateMachine/tests/Unit/WinzouStateMachineAdapterTest.php new file mode 100644 index 0000000000..897540f896 --- /dev/null +++ b/src/Sylius/Abstraction/StateMachine/tests/Unit/WinzouStateMachineAdapterTest.php @@ -0,0 +1,199 @@ +winzouStateMachine = $this->createMock(WinzouStateMachine::class); + $this->setStateMachineConfig($this->winzouStateMachine, [ + 'transitions' => [ + 'transition' => [ + 'from' => ['from_state'], + 'to' => 'to_state', + ], + 'another_transition' => [ + 'from' => ['another_from_state'], + 'to' => 'another_to_state', + ], + ], + ]); + + $this->winzouStateMachineFactory = $this->createMock(FactoryInterface::class); + $this->winzouStateMachineFactory + ->method('get') + ->willReturn($this->winzouStateMachine) + ; + } + + public function testItReturnWhetherTransitionCanBeApplied(): void + { + $this->winzouStateMachine->method('can')->willReturn(true); + + $subject = new \stdClass(); + $graphName = 'graph_name'; + $transition = 'transition'; + + $this->assertTrue($this->createTestSubject()->can($subject, $graphName, $transition)); + } + + public function testItAppliesTransition(): void + { + $this->winzouStateMachine->expects($this->once())->method('apply'); + + $subject = new \stdClass(); + $graphName = 'graph_name'; + $transition = 'transition'; + + $this->createTestSubject()->apply($subject, $graphName, $transition); + } + + public function testItReturnsEnabledTransitions(): void + { + $subject = new \stdClass(); + $graphName = 'graph_name'; + + $this->winzouStateMachine->method('can')->willReturnMap([ + ['transition', true], + ['another_transition', false], + ]); + + $transitions = $this->createTestSubject()->getEnabledTransitions($subject, $graphName); + + $this->assertCount(1, $transitions); + $this->assertSame('transition', $transitions[0]->getName()); + $this->assertSame(['from_state'], $transitions[0]->getFroms()); + $this->assertSame(['to_state'], $transitions[0]->getTos()); + } + + public function testItConvertsWorkflowExceptionsToCustomOnesOnCan(): void + { + $this->expectException(StateMachineExecutionException::class); + + $subject = new \stdClass(); + $graphName = 'graph_name'; + $transition = 'transition'; + + $this->winzouStateMachineFactory->method('get')->willThrowException(new SMException()); + + $this->createTestSubject()->can($subject, $graphName, $transition); + } + + public function testItConvertsWorkflowExceptionsToCustomOnApply(): void + { + $this->expectException(StateMachineExecutionException::class); + + $subject = new \stdClass(); + $graphName = 'graph_name'; + $transition = 'transition'; + + $this->winzouStateMachineFactory->method('get')->willThrowException(new SMException()); + + $this->createTestSubject()->apply($subject, $graphName, $transition); + } + + public function testItConvertsWorkflowExceptionsToCustomOnGetEnabledTransitions(): void + { + $this->expectException(StateMachineExecutionException::class); + + $subject = new \stdClass(); + $graphName = 'graph_name'; + + $this->winzouStateMachineFactory->method('get')->willThrowException(new SMException()); + + $this->createTestSubject()->getEnabledTransitions($subject, $graphName); + } + + public function testItReturnsTransitionsToForGivenTransition(): void + { + $this->setStateMachineConfig($this->winzouStateMachine, [ + 'transitions' => [ + 'transition_to_state' => [ + 'from' => ['from_state'], + 'to' => 'to_state', + ], + ], + ]); + + $this->winzouStateMachine->method('can')->willReturn(true); + + $this->winzouStateMachineFactory = $this->createMock(FactoryInterface::class); + $this->winzouStateMachineFactory->method('get')->willReturn($this->winzouStateMachine); + + $stateMachine = $this->createTestSubject(); + + $this->assertSame( + 'transition_to_state', + $stateMachine->getTransitionToState(new \stdClass(), 'graph_name', 'to_state'), + ); + } + + public function testItReturnsTransitionsFromForGivenTransition(): void + { + $this->setStateMachineConfig($this->winzouStateMachine, [ + 'transitions' => [ + 'transition_from_state' => [ + 'from' => ['from_state'], + 'to' => 'to_state', + ], + ], + ]); + + $this->winzouStateMachine->method('can')->willReturn(true); + + $this->winzouStateMachineFactory = $this->createMock(FactoryInterface::class); + $this->winzouStateMachineFactory->method('get')->willReturn($this->winzouStateMachine); + + $stateMachine = $this->createTestSubject(); + + $this->assertSame( + 'transition_from_state', + $stateMachine->getTransitionFromState(new \stdClass(), 'graph_name', 'from_state'), + ); + } + + private function createTestSubject(): StateMachineInterface + { + return new WinzouStateMachineAdapter($this->winzouStateMachineFactory); + } + + /** + * @param array $config + */ + private function setStateMachineConfig(WinzouStateMachine $stateMachine, array $config): void + { + $reflection = new \ReflectionClass($stateMachine); + $configProperty = $reflection->getProperty('config'); + $configProperty->setAccessible(true); + $configProperty->setValue($stateMachine, $config); + } +} diff --git a/src/Sylius/Behat/Client/ApiClientInterface.php b/src/Sylius/Behat/Client/ApiClientInterface.php index c13907357d..4cb5f279d2 100644 --- a/src/Sylius/Behat/Client/ApiClientInterface.php +++ b/src/Sylius/Behat/Client/ApiClientInterface.php @@ -18,21 +18,24 @@ use Symfony\Component\HttpFoundation\Response; interface ApiClientInterface { - public function request(RequestInterface $request): Response; + public function request(RequestInterface $request, bool $forgetResponse = true): Response; - public function index(string $resource): Response; + /** + * @param array $queryParameters + */ + public function index(string $resource, array $queryParameters = [], bool $forgetResponse = false): Response; - public function showByIri(string $iri): Response; + public function showByIri(string $iri, bool $forgetResponse = false): Response; - public function subResourceIndex(string $resource, string $subResource, string $id): Response; + public function subResourceIndex(string $resource, string $subResource, string $id, array $queryParameters = [], bool $forgetResponse = false): Response; - public function show(string $resource, string $id): Response; + public function show(string $resource, string $id, bool $forgetResponse = false): Response; - public function create(?RequestInterface $request = null): Response; + public function create(?RequestInterface $request = null, bool $forgetResponse = false): Response; - public function update(): Response; + public function update(bool $forgetResponse = false): Response; - public function delete(string $resource, string $id): Response; + public function delete(string $resource, string $id, bool $forgetResponse = false): Response; public function filter(): Response; @@ -56,13 +59,15 @@ interface ApiClientInterface public function addParameter(string $key, int|string $value): void; - public function addFilter(string $key, int|string|bool $value): void; + public function addFilter(string $key, bool|int|string $value): void; public function clearParameters(): void; public function addFile(string $key, UploadedFile $file): void; - public function addRequestData(string $key, string|int|bool|array $value): void; + public function addRequestData(string $key, array|bool|int|string|null $value): void; + + public function replaceRequestData(string $key, array|bool|int|string|null $value): void; public function setSubResourceData(string $key, array $data): void; diff --git a/src/Sylius/Behat/Client/ApiPlatformClient.php b/src/Sylius/Behat/Client/ApiPlatformClient.php index 8f8294ca6e..2d59183e89 100644 --- a/src/Sylius/Behat/Client/ApiPlatformClient.php +++ b/src/Sylius/Behat/Client/ApiPlatformClient.php @@ -23,6 +23,8 @@ final class ApiPlatformClient implements ApiClientInterface { private ?RequestInterface $request = null; + private ?Response $lastResponse = null; + public function __construct( private AbstractBrowser $client, private SharedStorageInterface $sharedStorage, @@ -32,30 +34,33 @@ final class ApiPlatformClient implements ApiClientInterface ) { } - public function index(string $resource): Response + public function index(string $resource, array $queryParameters = [], bool $forgetResponse = false): Response { - $this->request = $this->requestFactory->index($this->section, $resource, $this->authorizationHeader, $this->getToken()); + $this->request = $this + ->requestFactory + ->index($this->section, $resource, $this->authorizationHeader, $this->getToken(), $queryParameters) + ; - return $this->request($this->request); + return $this->request($this->request, $forgetResponse); } - public function showByIri(string $iri): Response + public function showByIri(string $iri, bool $forgetResponse = false): Response { $request = $this->requestFactory->custom($iri, HttpRequest::METHOD_GET); $request->authorize($this->getToken(), $this->authorizationHeader); - return $this->request($request); + return $this->request($request, $forgetResponse); } - public function subResourceIndex(string $resource, string $subResource, string $id): Response + public function subResourceIndex(string $resource, string $subResource, string $id, array $queryParameters = [], bool $forgetResponse = false): Response { - $request = $this->requestFactory->subResourceIndex($this->section, $resource, $id, $subResource); + $request = $this->requestFactory->subResourceIndex($this->section, $resource, $id, $subResource, $queryParameters); $request->authorize($this->getToken(), $this->authorizationHeader); - return $this->request($request); + return $this->request($request, $forgetResponse); } - public function show(string $resource, string $id): Response + public function show(string $resource, string $id, bool $forgetResponse = false): Response { return $this->request( $this->requestFactory->show( @@ -65,25 +70,26 @@ final class ApiPlatformClient implements ApiClientInterface $this->authorizationHeader, $this->getToken(), ), + $forgetResponse, ); } - public function create(?RequestInterface $request = null): Response + public function create(?RequestInterface $request = null, bool $forgetResponse = false): Response { - return $this->request($request ?? $this->request); + return $this->request($request ?? $this->request, $forgetResponse); } - public function update(): Response + public function update(bool $forgetResponse = false): Response { - return $this->request($this->request); + return $this->request($this->request, $forgetResponse); } - public function resend(): Response + public function resend(bool $forgetResponse = false): Response { - return $this->request($this->request); + return $this->request($this->request, $forgetResponse); } - public function delete(string $resource, string $id): Response + public function delete(string $resource, string $id, bool $forgetResponse = false): Response { return $this->request( $this->requestFactory->delete( @@ -93,6 +99,7 @@ final class ApiPlatformClient implements ApiClientInterface $this->authorizationHeader, $this->getToken(), ), + $forgetResponse, ); } @@ -197,12 +204,18 @@ final class ApiPlatformClient implements ApiClientInterface $this->request->updateFiles([$key => $file]); } - /** @param string|int|bool|array $value */ - public function addRequestData(string $key, $value): void + public function addRequestData(string $key, array|bool|int|string|null $value): void { $this->request->updateContent([$key => $value]); } + public function replaceRequestData(string $key, array|bool|int|string|null $value): void + { + $requestContent = $this->request->getContent(); + + $this->request->setContent(array_replace($requestContent, [$key => $value])); + } + public function updateRequestData(array $data): void { $this->request->updateContent($data); @@ -230,7 +243,11 @@ final class ApiPlatformClient implements ApiClientInterface public function getLastResponse(): Response { - return $this->client->getResponse(); + if (null === $this->lastResponse) { + throw new \RuntimeException('There is no last response.'); + } + + return $this->lastResponse; } public function getToken(): ?string @@ -238,7 +255,7 @@ final class ApiPlatformClient implements ApiClientInterface return $this->sharedStorage->has('token') ? $this->sharedStorage->get('token') : null; } - public function request(RequestInterface $request): Response + public function request(RequestInterface $request, bool $forgetResponse = false): Response { $this->setServerParameters(); @@ -251,7 +268,14 @@ final class ApiPlatformClient implements ApiClientInterface $request->content() ?? null, ); - return $this->getLastResponse(); + /** @var Response $response */ + $response = $this->client->getResponse(); + + if (false === $forgetResponse) { + $this->lastResponse = $response; + } + + return $response; } private function setServerParameters(): void diff --git a/src/Sylius/Behat/Client/Request.php b/src/Sylius/Behat/Client/Request.php index 7ab48dd045..a1fdcbc537 100644 --- a/src/Sylius/Behat/Client/Request.php +++ b/src/Sylius/Behat/Client/Request.php @@ -114,6 +114,13 @@ final class Request implements RequestInterface private function mergeArraysUniquely(array $firstArray, array $secondArray): array { foreach ($secondArray as $key => $value) { + if (is_string($key) && str_ends_with($key, '[]')) { + $key = substr($key, 0, -2); + $firstArray[$key][] = $value; + + continue; + } + if (is_array($value) && is_array(@$firstArray[$key])) { $value = $this->mergeArraysUniquely($firstArray[$key], $value); } diff --git a/src/Sylius/Behat/Client/RequestBuilder.php b/src/Sylius/Behat/Client/RequestBuilder.php index 61b32080de..cac828c1ca 100644 --- a/src/Sylius/Behat/Client/RequestBuilder.php +++ b/src/Sylius/Behat/Client/RequestBuilder.php @@ -17,10 +17,6 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; final class RequestBuilder { - private string $uri; - - private string $method; - private array $parameters = []; private array $headers = []; @@ -29,10 +25,10 @@ final class RequestBuilder private array $files = []; - private function __construct(string $uri, string $method) - { - $this->uri = $uri; - $this->method = $method; + private function __construct( + private string $uri, + private string $method, + ) { } public static function create(string $uri, string $method): self @@ -61,7 +57,7 @@ final class RequestBuilder return $this; } - public function withParameter(string $key, string $value): self + public function withParameter(string $key, array|string $value): self { $this->parameters[$key] = $value; diff --git a/src/Sylius/Behat/Client/RequestFactory.php b/src/Sylius/Behat/Client/RequestFactory.php index 48ad83bad5..5c299dfb20 100644 --- a/src/Sylius/Behat/Client/RequestFactory.php +++ b/src/Sylius/Behat/Client/RequestFactory.php @@ -32,9 +32,10 @@ final class RequestFactory implements RequestFactoryInterface string $resource, string $authorizationHeader, ?string $token = null, + array $queryParameters = [], ): RequestInterface { $builder = RequestBuilder::create( - sprintf('%s/%s/%s', $this->apiUrlPrefix, $section, $resource), + sprintf('%s/%s/%s%s', $this->apiUrlPrefix, $section, $resource, $this->getQueryString($queryParameters)), HttpRequest::METHOD_GET, ); $builder->withHeader('HTTP_ACCEPT', self::LINKED_DATA_JSON_CONTENT_TYPE); @@ -46,10 +47,23 @@ final class RequestFactory implements RequestFactoryInterface return $builder->build(); } - public function subResourceIndex(string $section, string $resource, string $id, string $subResource): RequestInterface - { + public function subResourceIndex( + string $section, + string $resource, + string $id, + string $subResource, + array $queryParameters = [], + ): RequestInterface { $builder = RequestBuilder::create( - sprintf('%s/%s/%s/%s/%s', $this->apiUrlPrefix, $section, $resource, $id, $subResource), + sprintf( + '%s/%s/%s/%s/%s%s', + $this->apiUrlPrefix, + $section, + $resource, + $id, + $subResource, + $this->getQueryString($queryParameters), + ), HttpRequest::METHOD_GET, ); $builder->withHeader('HTTP_ACCEPT', self::LINKED_DATA_JSON_CONTENT_TYPE); @@ -195,4 +209,9 @@ final class RequestFactory implements RequestFactoryInterface return $builder->build(); } + + private function getQueryString(array $queryParameters): string + { + return count($queryParameters) > 0 ? '?' . http_build_query($queryParameters) : ''; + } } diff --git a/src/Sylius/Behat/Client/RequestFactoryInterface.php b/src/Sylius/Behat/Client/RequestFactoryInterface.php index 9e5c885659..4d221ce906 100644 --- a/src/Sylius/Behat/Client/RequestFactoryInterface.php +++ b/src/Sylius/Behat/Client/RequestFactoryInterface.php @@ -22,11 +22,15 @@ interface RequestFactoryInterface ?string $token = null, ): RequestInterface; + /** + * @param array $queryParameters + */ public function subResourceIndex( string $section, string $resource, string $id, string $subResource, + array $queryParameters = [], ): RequestInterface; public function show( diff --git a/src/Sylius/Behat/Client/ResponseChecker.php b/src/Sylius/Behat/Client/ResponseChecker.php index da5e120b80..b1588ec869 100644 --- a/src/Sylius/Behat/Client/ResponseChecker.php +++ b/src/Sylius/Behat/Client/ResponseChecker.php @@ -54,13 +54,17 @@ final class ResponseChecker implements ResponseCheckerInterface return $translations[$localeCode][$key]; } - public function getError(Response $response): string + public function getError(Response $response): ?string { if ($this->hasKey($response, 'message')) { return $this->getValue($response, 'message'); } - return $this->getResponseContentValue($response, 'hydra:description'); + if ($this->hasKey($response, 'hydra:description')) { + return $this->getResponseContentValue($response, 'hydra:description'); + } + + return $response->getContent(); } public function isAccepted(Response $response): bool @@ -128,18 +132,50 @@ final class ResponseChecker implements ResponseCheckerInterface return false; } - /** @param string|int $value */ - public function hasSubResourceWithValue(Response $response, string $subResource, string $key, $value): bool - { - foreach ($this->getResponseContentValue($response, $subResource) as $resource) { - if ($resource[$key] === $value) { - return true; + public function hasValuesInAnySubresourceObjectCollection( + Response $response, + string $subResource, + array $expectedValues, + ): bool { + $resourceCollection = $this->getResponseContentValue($response, $subResource); + + $this->assertIsArray($resourceCollection); + + foreach ($resourceCollection as $resource) { + $this->assertIsArray($resource); + + foreach ($expectedValues as $key => $expectedValue) { + if (!array_key_exists($key, $resource) || $resource[$key] !== $expectedValue) { + continue 2; + } } + + return true; } return false; } + public function hasValuesInSubresourceObject( + Response $response, + string $subResource, + array $expectedValues, + ): bool { + $resource = $this->getResponseContentValue($response, $subResource); + + $this->assertIsArray($resource); + + $this->assertAllExpectedKeysArePresent($expectedValues, $resource); + + foreach ($expectedValues as $key => $expectedValue) { + if ($resource[$key] !== $expectedValue) { + return false; + } + } + + return true; + } + /** @param string|array $value */ public function hasItemOnPositionWithValue(Response $response, int $position, string $key, $value): bool { @@ -230,9 +266,10 @@ final class ResponseChecker implements ResponseCheckerInterface Assert::keyExists( $content, $key, - SprintfResponseEscaper::provideMessageWithEscapedResponseContent( - 'Expected \'' . $key . '\' not found.', - $response, + sprintf( + 'Expected to get: "%s" key in response, got keys: [%s]', + $key, + implode(', ', array_keys($content)), ), ); @@ -249,4 +286,26 @@ final class ResponseChecker implements ResponseCheckerInterface return true; } + + private function assertIsArray(mixed $resource): void + { + Assert::isArray($resource, sprintf('Expected to get an array, got "%s"', gettype($resource))); + } + + /** + * @param array $expectedValues + * @param array $resource + */ + private function assertAllExpectedKeysArePresent(array $expectedValues, array $resource): void + { + Assert::count( + array_diff_key($expectedValues, $resource), + 0, + sprintf( + 'Expected values array has keys: [%s], that are not present in the responses keys: [%s]', + implode(', ', array_keys(array_diff_key($expectedValues, $resource))), + implode(', ', array_keys($resource)), + ), + ); + } } diff --git a/src/Sylius/Behat/Client/ResponseCheckerInterface.php b/src/Sylius/Behat/Client/ResponseCheckerInterface.php index 89267cb80e..b775005011 100644 --- a/src/Sylius/Behat/Client/ResponseCheckerInterface.php +++ b/src/Sylius/Behat/Client/ResponseCheckerInterface.php @@ -29,7 +29,7 @@ interface ResponseCheckerInterface public function getTranslationValue(Response $response, string $key, ?string $localeCode): string; - public function getError(Response $response): string; + public function getError(Response $response): ?string; public function isAccepted(Response $response): bool; @@ -51,7 +51,19 @@ interface ResponseCheckerInterface public function hasItemWithValue(Response $response, string $key, int|string $value): bool; - public function hasSubResourceWithValue(Response $response, string $subResource, string $key, int|string $value): bool; + /** @param array $expectedValues */ + public function hasValuesInAnySubresourceObjectCollection( + Response $response, + string $subResource, + array $expectedValues, + ): bool; + + /** @param array $expectedValues */ + public function hasValuesInSubresourceObject( + Response $response, + string $subResource, + array $expectedValues, + ): bool; public function hasItemOnPositionWithValue(Response $response, int $position, string $key, array|string $value): bool; diff --git a/src/Sylius/Behat/Context/Api/Admin/AjaxTaxonsContext.php b/src/Sylius/Behat/Context/Api/Admin/AjaxTaxonsContext.php new file mode 100644 index 0000000000..a352bf5acd --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/AjaxTaxonsContext.php @@ -0,0 +1,92 @@ +client->getCookieJar()->set(new Cookie($this->requestStack->getSession()->getName(), $this->requestStack->getSession()->getId())); + $this->client->request('GET', '/admin/ajax/taxons/search', ['phrase' => $phrase], [], ['ACCEPT' => 'application/json']); + } + + /** + * @When I want to get taxon with :code code + */ + public function iWantToGetTaxonWithCode(string $code): void + { + $this->client->getCookieJar()->set(new Cookie($this->requestStack->getSession()->getName(), $this->requestStack->getSession()->getId())); + $this->client->request('GET', '/admin/ajax/taxons/leaf', ['code' => $code], [], ['ACCEPT' => 'application/json']); + } + + /** + * @When /^I want to get children from (taxon "[^"]+")/ + */ + public function iWantToGetChildrenFromTaxon(TaxonInterface $taxon): void + { + $this->client->getCookieJar()->set(new Cookie($this->requestStack->getSession()->getName(), $this->requestStack->getSession()->getId())); + $this->client->request('GET', '/admin/ajax/taxons/leafs', ['parentCode' => $taxon->getCode()], [], ['ACCEPT' => 'application/json']); + } + + /** + * @When I want to get taxon root + */ + public function iWantToGetTaxonRoot(): void + { + $this->client->getCookieJar()->set(new Cookie($this->requestStack->getSession()->getName(), $this->requestStack->getSession()->getId())); + $this->client->request('GET', '/admin/ajax/taxons/root-nodes', [], [], ['ACCEPT' => 'application/json']); + } + + /** + * @Then /^I should see (\d+) taxons on the list$/ + */ + public function iShouldSeeTaxonsInTheList(int $number): void + { + $response = $this->responseChecker->getResponseContent($this->client->getResponse()); + + Assert::eq(count($response), $number); + } + + /** + * @Then I should see the taxon named :firstName in the list + * @Then I should see the taxon named :firstName and :secondName in the list + * @Then I should see the taxon named :firstName, :secondName and :thirdName in the list + * @Then I should see the taxon named :firstName, :secondName, :thirdName and :fourthName in the list + */ + public function iShouldSeeTheTaxonNamedAnd(string ...$expectedTaxonNames): void + { + $response = $this->responseChecker->getResponseContent($this->client->getResponse()); + $taxonNames = array_column($response, 'name'); + + Assert::allOneOf($expectedTaxonNames, $taxonNames); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php index 3567ce9e04..734ae6239f 100644 --- a/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/BrowsingCatalogPromotionProductVariantsContext.php @@ -13,12 +13,15 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; use Sylius\Component\Core\Model\CatalogPromotionInterface; +use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Model\ProductVariantInterface; use Webmozart\Assert\Assert; final class BrowsingCatalogPromotionProductVariantsContext implements Context @@ -36,7 +39,18 @@ final class BrowsingCatalogPromotionProductVariantsContext implements Context public function iBrowseVariantsAffectedByCatalogPromotion(CatalogPromotionInterface $catalogPromotion): void { $this->client->index(Resources::PRODUCT_VARIANTS); - $this->client->addFilter('catalogPromotion', $this->iriConverter->getIriFromItem($catalogPromotion)); + $this->client->addFilter('catalogPromotion', $this->iriConverter->getIriFromResource($catalogPromotion)); + $this->client->filter(); + } + + /** + * @When /^I want to view all variants of (this product)$/ + * @When /^I view(?:| all) variants of the (product "[^"]+")$/ + */ + public function iWantToViewAllVariantsOfThisProduct(ProductInterface $product): void + { + $this->client->index(Resources::PRODUCT_VARIANTS); + $this->client->addFilter('product', $this->iriConverter->getIriFromResource($product)); $this->client->filter(); } @@ -66,4 +80,71 @@ final class BrowsingCatalogPromotionProductVariantsContext implements Context )); } } + + /** + * @Then :variant variant price should be decreased by catalog promotion :catalogPromotion in :channel channel + */ + public function variantPriceShouldBeDecreasedByCatalogPromotion( + ProductVariantInterface $variant, + CatalogPromotionInterface $catalogPromotion, + ChannelInterface $channel, + ): void { + Assert::true( + $this->variantHasCatalogPromotionInChannel($variant, $catalogPromotion, $channel), + sprintf( + 'Catalog promotion "%s" was not found in applied promotions of variant "%s" in channel "%s".', + $catalogPromotion->getCode(), + $variant->getCode(), + $channel->getCode(), + ), + ); + } + + /** + * @Then :variant variant price should not be decreased by catalog promotion :catalogPromotion in :channel channel + */ + public function variantPriceShouldNotBeDecreasedByCatalogPromotion( + ProductVariantInterface $variant, + CatalogPromotionInterface $catalogPromotion, + ChannelInterface $channel, + ): void { + Assert::false( + $this->variantHasCatalogPromotionInChannel($variant, $catalogPromotion, $channel), + sprintf( + 'Catalog promotion "%s" was found in applied promotions of variant "%s" in channel "%s".', + $catalogPromotion->getCode(), + $variant->getCode(), + $channel->getCode(), + ), + ); + } + + private function variantHasCatalogPromotionInChannel( + ProductVariantInterface $variant, + CatalogPromotionInterface $catalogPromotion, + ChannelInterface $channel, + ): bool { + $variantData = $this->getDataOfVariantWithCode($variant->getCode()); + + $promotions = $variantData['channelPricings'][$channel->getCode()]['appliedPromotions'] ?? []; + foreach ($promotions as $promotion) { + if ($promotion['code'] === $catalogPromotion->getCode()) { + return true; + } + } + + return false; + } + + private function getDataOfVariantWithCode(string $code): array + { + $variantsData = $this->responseChecker->getCollection($this->client->getLastResponse()); + foreach ($variantsData as $variantData) { + if ($variantData['code'] === $code) { + return $variantData; + } + } + + throw new \InvalidArgumentException(sprintf('Variant with code "%s" was not found.', $code)); + } } diff --git a/src/Sylius/Behat/Context/Api/Admin/BrowsingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/BrowsingProductVariantsContext.php new file mode 100644 index 0000000000..a73e2a6415 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/BrowsingProductVariantsContext.php @@ -0,0 +1,97 @@ +client->index( + Resources::PRODUCT_VARIANTS, + [ + 'order[position]' => 'desc', + ], + ); + } + + /** + * @When I set the position of :productVariant to :position + */ + public function iSetThePositionOfTo(ProductVariantInterface $productVariant, int $position): void + { + $this->client->buildUpdateRequest(Resources::PRODUCT_VARIANTS, $productVariant->getCode()); + $this->client->updateRequestData(['position' => $position]); + } + + /** + * @When I save my new configuration + */ + public function iSaveMyNewConfiguration(): void + { + $this->client->update(); + } + + /** + * @Then the first variant in the list should have name :variantName + */ + public function theFirstVariantInTheListShouldHaveName(string $variantName): void + { + $variants = $this->responseChecker->getCollection($this->client->getLastResponse()); + + $firstVariant = reset($variants); + + $this->assertProductVariantName($firstVariant['translations']['en_US']['name'], $variantName); + } + + /** + * @Then the last variant in the list should have name :variantName + */ + public function theLastVariantInTheListShouldHaveName(string $variantName): void + { + $variants = $this->responseChecker->getCollection($this->client->getLastResponse()); + + $lastVariant = end($variants); + + $this->assertProductVariantName($lastVariant['translations']['en_US']['name'], $variantName); + } + + private function assertProductVariantName(string $variantName, string $expectedVariantName): void + { + Assert::same( + $variantName, + $expectedVariantName, + sprintf( + 'Expected product variant to have name "%s", but it is named "%s".', + $expectedVariantName, + $variantName, + ), + ); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ChannelPricingLogEntryContext.php b/src/Sylius/Behat/Context/Api/Admin/ChannelPricingLogEntryContext.php new file mode 100644 index 0000000000..b9a9177380 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ChannelPricingLogEntryContext.php @@ -0,0 +1,90 @@ +sharedStorage->get('channel'); + Assert::notNull($channel); + + $this->sharedStorage->set('variant', $productVariant); + + $this->client->index(Resources::CHANNEL_PRICING_LOG_ENTRIES); + $this->client->addFilter('channelPricing.channelCode', $channel->getCode()); + $this->client->addFilter('channelPricing.productVariant.code', $productVariant->getCode()); + $this->client->filter(); + } + + /** + * @Then I should see :count log entries in the catalog price history + * @Then I should see a single log entry in the catalog price history + */ + public function iShouldSeeLogEntriesInTheCatalogPriceHistoryForTheVariant(int $count = 1): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $count); + } + + /** + * @Then /^there should be a log entry on the (\d+)(?:|st|nd|rd|th) position with the ("[^"]+") selling price, (no|"[^"]+") original price and datetime of the price change$/ + */ + public function thereShouldBeALogEntryOnThePositionWithTheSellingPriceOriginalPriceAndDatetimeOfThePriceChange( + int $position, + int $price, + int|string $originalPrice, + ): void { + if ('no' === $originalPrice) { + $originalPrice = null; + } + + $logEntry = $this->responseChecker->getCollection($this->client->getLastResponse())[$position - 1]; + + Assert::same($logEntry['price'], $price); + Assert::same($logEntry['originalPrice'], $originalPrice); + Assert::keyExists($logEntry, 'loggedAt'); + } + + /** + * @Then /^there should be a log entry with the ("[^"]+") selling price, (no|"[^"]+") original price and datetime of the price change$/ + */ + public function thereShouldBeALogEntryWithTheSellingPriceOriginalPriceAndDatetimeOfThePriceChange( + int $price, + int|string $originalPrice, + ): void { + $this->thereShouldBeALogEntryOnThePositionWithTheSellingPriceOriginalPriceAndDatetimeOfThePriceChange( + 1, + $price, + $originalPrice, + ); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/CreatingProductVariantContext.php b/src/Sylius/Behat/Context/Api/Admin/CreatingProductVariantContext.php new file mode 100644 index 0000000000..423803d6ad --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/CreatingProductVariantContext.php @@ -0,0 +1,52 @@ +client->buildCreateRequest(Resources::PRODUCT_VARIANTS); + $this->client->addRequestData('product', $this->iriConverter->getIriFromResource($product)); + $this->client->addRequestData('code', StringInflector::nameToCode($name)); + + $this->client->addRequestData('channelPricings', [ + $channel->getCode() => [ + 'price' => $price, + 'channelCode' => $channel->getCode(), + ], + ]); + + $this->client->create(); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/DashboardContext.php b/src/Sylius/Behat/Context/Api/Admin/DashboardContext.php new file mode 100644 index 0000000000..03867a33cf --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/DashboardContext.php @@ -0,0 +1,157 @@ +client->index('statistics'); + } + + /** + * @When I view statistics for :channel channel and current year split by month + * @When I choose :channel channel + * @When I view statistics for :channel channel + */ + public function iViewStatisticsForChannelAndYear(ChannelInterface $channel): void + { + $this->client->index( + 'statistics', + [ + 'channelCode' => $channel->getCode(), + 'startDate' => $this->dateTimeProvider->now()->format('Y-01-01\T00:00:00'), + 'interval' => 'month', + 'endDate' => $this->dateTimeProvider->now()->format('Y-12-31\T23:59:59'), + ], + ); + } + + /** + * @When I view statistics for :channel channel and previous year split by month + */ + public function iViewStatisticsForChannelAndPreviousYear(ChannelInterface $channel): void + { + $currentYear = (int) $this->dateTimeProvider->now()->format('Y'); + + $this->client->index( + 'statistics', + [ + 'channelCode' => $channel->getCode(), + 'startDate' => ($currentYear - 1) . '-01-01T00:00:00', + 'interval' => 'month', + 'endDate' => ($currentYear - 1) . '-12-31T23:59:59', + ], + ); + } + + /** + * @When I view statistics for :channel channel and next year split by month + */ + public function iViewStatisticsForChannelAndNextYear(ChannelInterface $channel): void + { + $currentYear = (int) $this->dateTimeProvider->now()->format('Y'); + + $this->client->index( + 'statistics', + [ + 'channelCode' => $channel->getCode(), + 'startDate' => ($currentYear + 1) . '-01-01T00:00:00', + 'interval' => 'month', + 'endDate' => ($currentYear + 1) . '-12-31T23:59:59', + ], + ); + } + + /** + * @Then I should see :count paid orders + */ + public function iShouldSeeNewOrders(int $count): void + { + Assert::true( + $this->responseChecker->hasValuesInSubresourceObject( + $this->client->getLastResponse(), + 'businessActivitySummary', + ['paidOrdersCount' => $count], + ), + ); + } + + /** + * @Then I should see :number new customers( in the list) + */ + public function iShouldSeeNewCustomers(int $count): void + { + Assert::true( + $this->responseChecker->hasValuesInSubresourceObject( + $this->client->getLastResponse(), + 'businessActivitySummary', + ['newCustomersCount' => $count], + ), + sprintf( + 'There should be %s new customers, but got %s.', + $count, + json_encode( + $this->responseChecker->getValue($this->client->getLastResponse(), 'businessActivitySummary'), + ), + ), + ); + } + + /** + * @Then /^there should be total sales of ("[^"]+")$/ + */ + public function thereShouldBeTotalSalesOf(int $totalSales): void + { + Assert::true( + $this->responseChecker->hasValuesInSubresourceObject( + $this->client->getLastResponse(), + 'businessActivitySummary', + ['totalSales' => $totalSales], + ), + ); + } + + /** + * @Then /^the average order value should be ("[^"]+")$/ + */ + public function myAverageOrderValueShouldBe(int $averageTotalValue): void + { + Assert::true( + $this->responseChecker->hasValuesInSubresourceObject( + $this->client->getLastResponse(), + 'businessActivitySummary', + ['averageOrderValue' => $averageTotalValue], + ), + sprintf('Average order value should be %s, but it does not.', $averageTotalValue), + ); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingAdministratorsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingAdministratorsContext.php index d76b8d11fa..060409d72c 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingAdministratorsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingAdministratorsContext.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\RequestBuilder; @@ -127,15 +127,6 @@ final class ManagingAdministratorsContext implements Context $this->client->create(); } - /** - * @When I save my changes - */ - public function iSaveMyChanges(): void - { - $response = $this->client->update(); - $this->responseChecker->isUpdateSuccessful($response); - } - /** * @When I delete administrator with email :adminUser */ @@ -156,7 +147,7 @@ final class ManagingAdministratorsContext implements Context $builder->withHeader('CONTENT_TYPE', 'multipart/form-data'); $builder->withHeader('HTTP_ACCEPT', 'application/ld+json'); $builder->withHeader('HTTP_Authorization', 'Bearer ' . $this->sharedStorage->get('token')); - $builder->withParameter('owner', $this->iriConverter->getIriFromItem($administrator)); + $builder->withParameter('owner', $this->iriConverter->getIriFromResource($administrator)); $builder->withFile('file', new UploadedFile($this->minkParameters['files_path'] . $avatar, basename($avatar))); $response = $this->client->request($builder->build()); @@ -245,17 +236,6 @@ final class ManagingAdministratorsContext implements Context ); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Administrator could not be edited', - ); - } - /** * @Then I should be notified that it has been successfully deleted */ diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingCatalogPromotionsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingCatalogPromotionsContext.php index 85e5ba5272..4610f3c0c1 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingCatalogPromotionsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingCatalogPromotionsContext.php @@ -13,11 +13,12 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Bundle\CoreBundle\CatalogPromotion\Calculator\FixedDiscountPriceCalculator; use Sylius\Bundle\CoreBundle\CatalogPromotion\Calculator\PercentageDiscountPriceCalculator; @@ -38,6 +39,7 @@ final class ManagingCatalogPromotionsContext implements Context private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, private SharedStorageInterface $sharedStorage, ) { } @@ -104,7 +106,6 @@ final class ManagingCatalogPromotionsContext implements Context */ public function iSpecifyItsAsIn(string $field, string $value, string $localeCode): void { - $data = ['translations' => [$localeCode => ['locale' => $localeCode]]]; $data['translations'][$localeCode][$field] = $value; $this->client->updateRequestData($data); @@ -131,7 +132,7 @@ final class ManagingCatalogPromotionsContext implements Context */ public function iDescribeItAsIn(string $description, string $localeCode): void { - $data = ['translations' => [$localeCode => ['locale' => $localeCode]]]; + $data = ['translations' => [$localeCode => []]]; $data['translations'][$localeCode]['description'] = $description; $this->client->updateRequestData($data); @@ -142,7 +143,7 @@ final class ManagingCatalogPromotionsContext implements Context */ public function iMakeItAvailableInChannel(ChannelInterface $channel): void { - $this->client->addRequestData('channels', [$this->iriConverter->getIriFromItemInSection($channel, 'admin')]); + $this->client->addRequestData('channels', [$this->sectionAwareIriConverter->getIriFromResourceInSection($channel, 'admin')]); } /** @@ -152,7 +153,7 @@ final class ManagingCatalogPromotionsContext implements Context { $channels = $this->responseChecker->getValue($this->client->show(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()), 'channels'); - foreach (array_keys($channels, $this->iriConverter->getIriFromItemInSection($channel, 'admin')) as $key) { + foreach (array_keys($channels, $this->sectionAwareIriConverter->getIriFromResourceInSection($channel, 'admin')) as $key) { unset($channels[$key]); } @@ -321,14 +322,6 @@ final class ManagingCatalogPromotionsContext implements Context $this->client->updateRequestData(['startDate' => $startDate]); } - /** - * @When I save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @When I browse catalog promotions */ @@ -491,18 +484,11 @@ final class ManagingCatalogPromotionsContext implements Context CatalogPromotionInterface $catalogPromotion, ProductVariantInterface $productVariant, ): void { - $this->client->buildUpdateRequest(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()); - $scopes = [[ - 'type' => InForVariantsScopeVariantChecker::TYPE, - 'configuration' => [ - 'variants' => [ - $productVariant->getCode(), - ], - ], - ]]; - - $this->client->updateRequestData(['scopes' => $scopes]); - $this->client->update(); + $this->changeFirstScopeConfigurationTo( + $catalogPromotion, + InForVariantsScopeVariantChecker::TYPE, + ['variants' => [$productVariant->getCode()]], + ); } /** @@ -512,16 +498,11 @@ final class ManagingCatalogPromotionsContext implements Context CatalogPromotionInterface $catalogPromotion, TaxonInterface $taxon, ): void { - $this->client->buildUpdateRequest(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()); - - $content = $this->client->getContent(); - unset($content['scopes'][0]['configuration']['variants']); - $content['scopes'][0]['type'] = InForTaxonsScopeVariantChecker::TYPE; - $content['scopes'][0]['configuration']['taxons'] = [$taxon->getCode()]; - - $this->client->setRequestData($content); - - $this->client->update(); + $this->changeFirstScopeConfigurationTo( + $catalogPromotion, + InForTaxonsScopeVariantChecker::TYPE, + ['taxons' => [$taxon->getCode()]], + ); } /** @@ -531,16 +512,11 @@ final class ManagingCatalogPromotionsContext implements Context CatalogPromotionInterface $catalogPromotion, ProductInterface $product, ): void { - $this->client->buildUpdateRequest(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()); - - $content = $this->client->getContent(); - unset($content['scopes'][0]['configuration']['variants']); - $content['scopes'][0]['type'] = InForProductScopeVariantChecker::TYPE; - $content['scopes'][0]['configuration']['products'] = [$product->getCode()]; - - $this->client->setRequestData($content); - - $this->client->update(); + $this->changeFirstScopeConfigurationTo( + $catalogPromotion, + InForProductScopeVariantChecker::TYPE, + ['products' => [$product->getCode()]], + ); } /** @@ -622,7 +598,7 @@ final class ManagingCatalogPromotionsContext implements Context $content['actions'] = [[ 'type' => PercentageDiscountPriceCalculator::TYPE, - 'configuration' => ['amount' => ''], + 'configuration' => ['amount' => null], ]]; $this->client->setRequestData($content); @@ -637,7 +613,7 @@ final class ManagingCatalogPromotionsContext implements Context $content['actions'] = [[ 'type' => FixedDiscountPriceCalculator::TYPE, - 'configuration' => [$channel->getCode() => ['amount' => '']], + 'configuration' => [$channel->getCode() => ['amount' => null]], ]]; $this->client->setRequestData($content); @@ -695,20 +671,22 @@ final class ManagingCatalogPromotionsContext implements Context { $actions = [[ 'type' => PercentageDiscountPriceCalculator::TYPE, - 'configuration' => [], + 'configuration' => [ + 'amount' => null, + ], ]]; $this->client->addRequestData('actions', $actions); } /** - * @When I add fixed discount action without amount configured + * @When I add fixed discount action without amount configured for the :channel channel */ - public function iAddFixedDiscountActionWithoutAmountConfigured(): void + public function iAddFixedDiscountActionWithoutAmountConfigured(ChannelInterface $channel): void { $actions = [[ 'type' => FixedDiscountPriceCalculator::TYPE, - 'configuration' => ['channel' => ['amount' => null]], + 'configuration' => [$channel->getCode() => ['amount' => null]], ]]; $this->client->addRequestData('actions', $actions); @@ -735,7 +713,7 @@ final class ManagingCatalogPromotionsContext implements Context { $actions = [[ 'type' => FixedDiscountPriceCalculator::TYPE, - 'configuration' => ['nonexistent_action' => ['amount' => 1000]], + 'configuration' => ['nonexistent_channel' => ['amount' => 1000]], ]]; $this->client->addRequestData('actions', $actions); @@ -767,7 +745,7 @@ final class ManagingCatalogPromotionsContext implements Context $this->client->buildUpdateRequest(Resources::CATALOG_PROMOTIONS, $catalogPromotionCode); $content = $this->client->getContent(); - foreach (array_keys($content['channels'], $this->iriConverter->getIriFromItem($channel)) as $key) { + foreach (array_keys($content['channels'], $this->iriConverter->getIriFromResource($channel)) as $key) { unset($content['channels'][$key]); } @@ -785,7 +763,7 @@ final class ManagingCatalogPromotionsContext implements Context ): void { $this->client->buildUpdateRequest(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()); $content = $this->client->getContent(); - $content['channels'][] = $this->iriConverter->getIriFromItem($channel); + $content['channels'][] = $this->iriConverter->getIriFromResource($channel); $this->client->updateRequestData(['channels' => $content['channels']]); $this->client->update(); } @@ -804,11 +782,11 @@ final class ManagingCatalogPromotionsContext implements Context $this->client->buildUpdateRequest(Resources::CATALOG_PROMOTIONS, $catalogPromotionCode); $content = $this->client->getContent(); - foreach (array_keys($content['channels'], $this->iriConverter->getIriFromItem($removedChannel)) as $key) { + foreach (array_keys($content['channels'], $this->iriConverter->getIriFromResource($removedChannel)) as $key) { unset($content['channels'][$key]); } - $content['channels'][] = $this->iriConverter->getIriFromItem($addedChannel); + $content['channels'][] = $this->iriConverter->getIriFromResource($addedChannel); $this->client->setRequestData($content); $this->client->update(); } @@ -845,7 +823,7 @@ final class ManagingCatalogPromotionsContext implements Context */ public function iFilterByChannel(ChannelInterface $channel): void { - $this->client->addFilter('channel', $this->iriConverter->getIriFromItem($channel)); + $this->client->addFilter('channel', $this->iriConverter->getIriFromResource($channel)); $this->client->filter(); } @@ -895,6 +873,18 @@ final class ManagingCatalogPromotionsContext implements Context $this->client->filter(); } + /** + * @When I sort catalog promotions by :order :field + */ + public function iSortCatalogPromotionByOrderField(string $order, string $field): void + { + $this->client->addFilter( + sprintf('order[%s]', lcfirst(str_replace(' ', '', ucwords($field)))), + $order === 'descending' ? 'desc' : 'asc', + ); + $this->client->filter(); + } + /** * @When I request the removal of :catalogPromotion catalog promotion */ @@ -1182,7 +1172,7 @@ final class ManagingCatalogPromotionsContext implements Context $this->responseChecker->hasValueInCollection( $this->client->show(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()), 'channels', - $this->iriConverter->getIriFromItemInSection($channel, 'admin'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($channel, 'admin'), ), sprintf('Catalog promotion is not assigned to %s channel', $channel->getName()), ); @@ -1201,7 +1191,7 @@ final class ManagingCatalogPromotionsContext implements Context $this->responseChecker->hasValueInCollection( $this->client->show(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()), 'channels', - $this->iriConverter->getIriFromItemInSection($channel, 'admin'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($channel, 'admin'), ), sprintf('Catalog promotion is assigned to %s channel', $channel->getName()), ); @@ -1218,17 +1208,6 @@ final class ManagingCatalogPromotionsContext implements Context ); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Catalog promotion could not be edited', - ); - } - /** * @Then I should be notified that not all channels are filled */ @@ -1236,7 +1215,7 @@ final class ManagingCatalogPromotionsContext implements Context { $response = $this->responseChecker->getResponseContent($this->client->getLastResponse()); - Assert::same($response['violations'][0]['message'], 'Configuration for one of the required channels is not provided.'); + Assert::same($response['violations'][0]['message'], 'This field is missing.'); } /** @@ -1280,7 +1259,7 @@ final class ManagingCatalogPromotionsContext implements Context ): void { $this->client->show(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()); - Assert::true($this->catalogPromotionAppliesOnVariants($productVariant)); + Assert::true($this->catalogPromotionHasValuesInScopeConfiguration('variants', $productVariant->getCode())); } /** @@ -1288,7 +1267,7 @@ final class ManagingCatalogPromotionsContext implements Context */ public function itShouldApplyOnVariant(ProductVariantInterface $variant): void { - Assert::true($this->catalogPromotionAppliesOnVariants($variant)); + Assert::true($this->catalogPromotionHasValuesInScopeConfiguration('variants', $variant->getCode())); } /** @@ -1296,7 +1275,7 @@ final class ManagingCatalogPromotionsContext implements Context */ public function itShouldApplyOnProduct(ProductInterface $product): void { - Assert::true($this->catalogPromotionAppliesOnProducts($product)); + Assert::true($this->catalogPromotionHasValuesInScopeConfiguration('products', $product->getCode())); } /** @@ -1308,7 +1287,7 @@ final class ManagingCatalogPromotionsContext implements Context ): void { $this->client->show(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()); - Assert::true($this->catalogPromotionAppliesOnTaxons($taxon)); + Assert::true($this->catalogPromotionHasValuesInScopeConfiguration('taxons', $taxon->getCode())); } /** @@ -1320,7 +1299,7 @@ final class ManagingCatalogPromotionsContext implements Context ): void { $this->client->show(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()); - Assert::false($this->catalogPromotionAppliesOnVariants($productVariant)); + Assert::false($this->catalogPromotionHasValuesInScopeConfiguration('variants', $productVariant->getCode())); } /** @@ -1332,7 +1311,7 @@ final class ManagingCatalogPromotionsContext implements Context ): void { $this->client->show(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()); - Assert::true($this->catalogPromotionAppliesOnProducts($product)); + Assert::true($this->catalogPromotionHasValuesInScopeConfiguration('products', $product->getCode())); } /** @@ -1401,13 +1380,24 @@ final class ManagingCatalogPromotionsContext implements Context } /** - * @Then /^I should be notified that type of (action|scope) is invalid$/ + * @Then /^I should be notified that type of action is invalid$/ */ - public function iShouldBeNotifiedThatTypeIsInvalid(string $field): void + public function iShouldBeNotifiedThatTypeOfActionIsInvalid(): void { Assert::contains( $this->responseChecker->getError($this->client->getLastResponse()), - sprintf('Catalog promotion %s type is invalid. Please choose a valid type.', $field), + 'Catalog promotion action type is invalid. Available types are fixed_discount, percentage_discount.', + ); + } + + /** + * @Then /^I should be notified that type of scope is invalid$/ + */ + public function iShouldBeNotifiedThatTypeOfScopeIsInvalid(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Catalog promotion scope type is invalid. Available types are for_products, for_taxons, for_variants.', ); } @@ -1423,10 +1413,9 @@ final class ManagingCatalogPromotionsContext implements Context } /** - * @Then I should be notified that a discount amount should be a number and cannot be empty - * @Then I should be notified that a discount amount is not valid + * @Then I should be notified that the percentage amount should be a number and cannot be empty */ - public function iShouldBeNotifiedThatDiscountAmountShouldBeNumber(): void + public function iShouldBeNotifiedThatDiscountAmountShouldBeANumberAndCannotBeEmpty(): void { Assert::contains( $this->responseChecker->getError($this->client->getLastResponse()), @@ -1435,9 +1424,9 @@ final class ManagingCatalogPromotionsContext implements Context } /** - * @Then I should be notified that a discount amount should be configured for at least one channel + * @Then I should be notified that the fixed amount should be a number and cannot be empty */ - public function iShouldBeNotifiedThatADiscountAmountShouldBeConfiguredForAtLeasOneChannel(): void + public function iShouldBeNotifiedThatTheFixedAmountShouldBeANumber(): void { Assert::contains( $this->responseChecker->getError($this->client->getLastResponse()), @@ -1463,7 +1452,7 @@ final class ManagingCatalogPromotionsContext implements Context { Assert::contains( $this->responseChecker->getError($this->client->getLastResponse()), - 'Provided configuration contains errors. Please add only existing variants.', + 'Product variant with code wrong_code does not exist.', ); } @@ -1483,9 +1472,9 @@ final class ManagingCatalogPromotionsContext implements Context */ public function iShouldBeNotifiedThatICanAddOnlyExisting(string $entity): void { - Assert::contains( + Assert::regex( $this->responseChecker->getError($this->client->getLastResponse()), - sprintf('Provided configuration contains errors. Please add only existing %ss.', $entity), + sprintf('/%s with code [^"]+ does not exist.$/', ucfirst($entity)), ); } @@ -1572,43 +1561,39 @@ final class ManagingCatalogPromotionsContext implements Context ); } - private function catalogPromotionAppliesOnVariants(ProductVariantInterface ...$productVariants): bool + /** + * @Then I should see :count catalog promotions on the list + */ + public function iShouldSeeCountCatalogPromotionsOnTheList(int $count): void { - $response = $this->responseChecker->getResponseContent($this->client->getLastResponse()); - - foreach ($productVariants as $productVariant) { - if (!isset($response['scopes'][0]['configuration']['variants'])) { - return false; - } - - if ($this->hasVariantInConfiguration($response['scopes'][0]['configuration']['variants'], $productVariant) === false) { - return false; - } - } - - return true; + Assert::count($this->responseChecker->getCollection($this->client->getLastResponse()), $count); } - private function catalogPromotionAppliesOnTaxons(TaxonInterface ...$taxons): bool + /** + * @Then the first catalog promotion should have code :code + */ + public function theFirstCatalogPromotionShouldHaveCode(string $code): void { - $response = $this->responseChecker->getResponseContent($this->client->getLastResponse()); + $catalogPromotions = $this->responseChecker->getCollection($this->client->getLastResponse()); - foreach ($taxons as $taxon) { - if ($this->hasTaxonInConfiguration($response['scopes'][0]['configuration']['taxons'], $taxon) === false) { - return false; - } - } - - return true; + Assert::same(reset($catalogPromotions)['code'], $code); } - private function catalogPromotionAppliesOnProducts(ProductInterface ...$products): bool + private function catalogPromotionHasValuesInScopeConfiguration(string $configurationKey, string ...$values): bool { $response = $this->responseChecker->getResponseContent($this->client->getLastResponse()); + $configuration = $response['scopes'] ?? []; + if ([] === $configuration || empty($configuration[0])) { + return false; + } - foreach ($products as $product) { - foreach ($response['scopes'] as $scope) { - if (isset($scope['configuration']['products']) && $this->hasProductInConfiguration($scope['configuration']['products'], $product) === true) { + foreach ($configuration as $scope) { + if (!isset($scope['configuration'][$configurationKey])) { + continue; + } + + foreach ($values as $value) { + if (in_array($value, $scope['configuration'][$configurationKey], true)) { return true; } } @@ -1617,39 +1602,6 @@ final class ManagingCatalogPromotionsContext implements Context return false; } - private function hasVariantInConfiguration(array $configuration, ProductVariantInterface $productVariant): bool - { - foreach ($configuration as $productVariantIri) { - if ($productVariantIri === $productVariant->getCode()) { - return true; - } - } - - return false; - } - - private function hasTaxonInConfiguration(array $configuration, TaxonInterface $taxon): bool - { - foreach ($configuration as $configuredTaxon) { - if ($configuredTaxon === $taxon->getCode()) { - return true; - } - } - - return false; - } - - private function hasProductInConfiguration(array $configuration, ProductInterface $product): bool - { - foreach ($configuration as $configuredProduct) { - if ($configuredProduct === $product->getCode()) { - return true; - } - } - - return false; - } - private function toggleCatalogPromotion(CatalogPromotionInterface $catalogPromotion, bool $enabled): void { $this->client->buildUpdateRequest(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()); @@ -1660,6 +1612,23 @@ final class ManagingCatalogPromotionsContext implements Context $this->sharedStorage->set('catalog_promotion', $catalogPromotion); } + private function changeFirstScopeConfigurationTo( + CatalogPromotionInterface $catalogPromotion, + string $type, + array $configuration, + ): void { + $this->client->buildUpdateRequest(Resources::CATALOG_PROMOTIONS, $catalogPromotion->getCode()); + + $content = $this->client->getContent(); + unset($content['scopes'][0]); + $content['scopes'][0]['type'] = $type; + $content['scopes'][0]['configuration'] = $configuration; + + $this->client->setRequestData($content); + + $this->client->update(); + } + private function createCatalogPromotion( string $name, int $priority, @@ -1675,10 +1644,9 @@ final class ManagingCatalogPromotionsContext implements Context 'name' => $name, 'priority' => $priority, 'enabled' => true, - 'channels' => [$this->iriConverter->getIriFromItem($channel)], + 'channels' => [$this->iriConverter->getIriFromResource($channel)], 'exclusive' => $exclusive, 'translations' => ['en_US' => [ - 'locale' => 'en_US', 'label' => $name, ]], 'actions' => [[ diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingChannelPriceHistoryConfigsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelPriceHistoryConfigsContext.php new file mode 100644 index 0000000000..4b229630c3 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelPriceHistoryConfigsContext.php @@ -0,0 +1,214 @@ +sharedStorage->set('channel', $channel); + $this->client->buildUpdateRequest( + Resources::CHANNEL_PRICE_HISTORY_CONFIGS, + (string) $channel->getChannelPriceHistoryConfig()->getId(), + ); + } + + /** + * @When /^I change showing of the lowest price of discounted products to be (enabled|disabled)$/ + */ + public function iChangeShowingOfTheLowestPriceOfDiscountedProducts(string $visible): void + { + $this->client->addRequestData( + 'lowestPriceForDiscountedProductsVisible', + $visible === 'enabled', + ); + } + + /** + * @When /^I change the lowest price for discounted products checking period to (-?\d+) days$/ + */ + public function iChangeTheLowestPriceForDiscountedProductsCheckingPeriodToDays(int $days): void + { + $this->client->addRequestData('lowestPriceForDiscountedProductsCheckingPeriod', $days); + } + + /** + * @When I exclude the :taxon taxon from showing the lowest price of discounted products + */ + public function iExcludeTheTaxonFromShowingTheLowestPriceOfDiscountedProducts(TaxonInterface $taxon): void + { + $this->iExcludeTheTaxonsFromShowingTheLowestPriceOfDiscountedProducts([$taxon]); + } + + /** + * @When /^I exclude the ("[^"]+" and "[^"]+" taxons) from showing the lowest price of discounted products$/ + */ + public function iExcludeTheTaxonsFromShowingTheLowestPriceOfDiscountedProducts(iterable $taxons): void + { + $taxonsIris = []; + foreach ($taxons as $taxon) { + $taxonsIris[] = $this->iriConverter->getIriFromResource($taxon); + } + + $this->client->addRequestData('taxonsExcludedFromShowingLowestPrice', $taxonsIris); + } + + /** + * @Then /^the ("[^"]+" channel) should have the lowest price of discounted products prior to the current discount (enabled|disabled)$/ + */ + public function theChannelShouldHaveTheLowestPriceOfDiscountedProductsPriorToTheCurrentDiscountEnabledOrDisabled( + ChannelInterface $channel, + string $visible, + ): void { + $lowestPriceForDiscountedProductsVisible = $this->responseChecker->getValue( + $this->client->show( + Resources::CHANNEL_PRICE_HISTORY_CONFIGS, + (string) $channel->getChannelPriceHistoryConfig()->getId(), + ), + 'lowestPriceForDiscountedProductsVisible', + ); + + Assert::same($lowestPriceForDiscountedProductsVisible, $visible === 'enabled'); + } + + /** + * @Then /^the ("[^"]+" channel) should have the lowest price for discounted products checking period set to (\d+) days$/ + * @Then /^(its) lowest price for discounted products checking period should be set to (\d+) days$/ + */ + public function theChannelShouldHaveTheLowestPriceForDiscountedProductsCheckingPeriodSetToDays( + ChannelInterface $channel, + int $days, + ): void { + $lowestPriceForDiscountedProductsCheckingPeriod = $this->responseChecker->getValue( + $this->client->show( + Resources::CHANNEL_PRICE_HISTORY_CONFIGS, + (string) $channel->getChannelPriceHistoryConfig()->getId(), + ), + 'lowestPriceForDiscountedProductsCheckingPeriod', + ); + + Assert::same($lowestPriceForDiscountedProductsCheckingPeriod, $days); + } + + /** + * @Then I should be notified that the lowest price for discounted products checking period must be greater than 0 + */ + public function iShouldBeNotifiedThatTheLowestPriceForDiscountedProductsCheckingPeriodMustBeGreaterThanZero(): void + { + Assert::true($this->responseChecker->hasViolationWithMessage( + $this->client->getLastResponse(), + 'Value must be greater than 0', + 'lowestPriceForDiscountedProductsCheckingPeriod', + )); + } + + /** + * @Then I should be notified that the lowest price for discounted products checking period must be lower + */ + public function iShouldBeNotifiedThatTheLowestPriceForDiscountedProductsCheckingPeriodMustBeLower(): void + { + Assert::true($this->responseChecker->hasViolationWithMessage( + $this->client->getLastResponse(), + 'Value must be less than 2147483647', + 'lowestPriceForDiscountedProductsCheckingPeriod', + )); + } + + /** + * @Then /^(this channel) should have ("[^"]+" taxon) excluded from displaying the lowest price of discounted products$/ + */ + public function thisChannelShouldHaveTaxonExcludedFromDisplayingTheLowestPriceOfDiscountedProducts( + ChannelInterface $channel, + TaxonInterface $taxon, + ): void { + $excludedTaxons = $this->responseChecker->getValue( + $this->client->show( + Resources::CHANNEL_PRICE_HISTORY_CONFIGS, + (string) $channel->getChannelPriceHistoryConfig()->getId(), + ), + 'taxonsExcludedFromShowingLowestPrice', + ); + + Assert::true($this->isResourceAdminIriInArray($taxon, $excludedTaxons)); + } + + /** + * @Then /^(this channel) should have ("([^"]+)" and "([^"]+)" taxons) excluded from displaying the lowest price of discounted products$/ + */ + public function thisChannelShouldHaveTaxonsExcludedFromDisplayingTheLowestPriceOfDiscountedProducts( + ChannelInterface $channel, + iterable $taxons, + ): void { + $excludedTaxons = $this->responseChecker->getValue( + $this->client->show( + Resources::CHANNEL_PRICE_HISTORY_CONFIGS, + (string) $channel->getChannelPriceHistoryConfig()->getId(), + ), + 'taxonsExcludedFromShowingLowestPrice', + ); + + foreach ($taxons as $taxon) { + Assert::true($this->isResourceAdminIriInArray($taxon, $excludedTaxons)); + } + } + + /** + * @Then /^(this channel) should not have ("[^"]+" taxon) excluded from displaying the lowest price of discounted products$/ + */ + public function thisChannelShouldNotHaveTaxonExcludedFromDisplayingTheLowestPriceOfDiscountedProducts( + ChannelInterface $channel, + TaxonInterface $taxon, + ): void { + $excludedTaxons = (array) $this->responseChecker->getValue( + $this->client->show( + Resources::CHANNEL_PRICE_HISTORY_CONFIGS, + (string) $channel->getChannelPriceHistoryConfig()->getId(), + ), + 'taxonsExcludedFromShowingLowestPrice', + ); + + Assert::false($this->isResourceAdminIriInArray($taxon, $excludedTaxons)); + } + + private function isResourceAdminIriInArray(ResourceInterface $resource, array $iris): bool + { + $iri = $this->sectionAwareIriConverter->getIriFromResourceInSection($resource, 'admin'); + + return in_array($iri, $iris, true); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsBillingDataContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsBillingDataContext.php new file mode 100644 index 0000000000..02ff55114c --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsBillingDataContext.php @@ -0,0 +1,83 @@ +getShopBillingDataFromChannel($channel); + + Assert::same($shopBillingData->getCompany(), $company); + } + + /** + * @Then /^(this channel) tax ID should be "([^"]+)"$/ + */ + public function thisChanneTaxIdShouldBe(ChannelInterface $channel, string $taxId): void + { + $shopBillingData = $this->getShopBillingDataFromChannel($channel); + + Assert::same($shopBillingData->getTaxId(), $taxId); + } + + /** + * @Then /^(this channel) shop billing address should be "([^"]+)", "([^"]+)" "([^"]+)" and ("([^"]+)" country)$/ + * @Then /^(this channel) shop billing address should still be "([^"]+)", "([^"]+)" "([^"]+)" and ("([^"]+)" country)$/ + */ + public function thisChannelShopBillingAddressShouldBe( + ChannelInterface $channel, + string $street, + string $postcode, + string $city, + CountryInterface $country, + ): void { + $shopBillingData = $this->getShopBillingDataFromChannel($channel); + + Assert::same($shopBillingData->getStreet(), $street); + Assert::same($shopBillingData->getPostcode(), $postcode); + Assert::same($shopBillingData->getCity(), $city); + Assert::same($shopBillingData->getCountryCode(), $country->getCode()); + } + + private function getShopBillingDataFromChannel(ChannelInterface $channel): ShopBillingDataInterface + { + $this->client->show(Resources::CHANNELS, $channel->getCode()); + + /** @var ShopBillingDataInterface $shopBillingData */ + $shopBillingData = $this->iriConverter->getResourceFromIri($this->responseChecker->getValue($this->client->getLastResponse(), 'shopBillingData')); + + return $shopBillingData; + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsContext.php index 8cd424f663..8546c610cf 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingChannelsContext.php @@ -13,12 +13,14 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Component\Addressing\Model\CountryInterface; +use Sylius\Component\Addressing\Model\ZoneInterface; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\TaxonInterface; @@ -34,6 +36,7 @@ final class ManagingChannelsContext implements Context private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, ) { } @@ -45,31 +48,122 @@ final class ManagingChannelsContext implements Context $this->client->buildCreateRequest(Resources::CHANNELS); } + /** + * @When I delete channel :channel + */ + public function iDeleteChannel(ChannelInterface $channel): void + { + $this->client->delete(Resources::CHANNELS, $channel->getCode()); + } + + /** + * @When I want to modify a channel :channel + */ + public function iWantToModifyChannel(ChannelInterface $channel): void + { + $this->client->buildUpdateRequest(Resources::CHANNELS, $channel->getCode()); + } + + /** + * @When I rename it to :name + * @When I do not name it + * @When I remove its name + */ + public function iRenameIt(string $name = ''): void + { + $this->client->addRequestData('name', $name); + } + + /** + * @When /^I (enable|disable) it$/ + */ + public function iDisableIt(string $toggleAction): void + { + $this->client->addRequestData('enabled', $toggleAction === 'enable'); + } + + /** + * @When I change its menu taxon to :taxon + */ + public function iChangeItsMenuTaxonTo(TaxonInterface $taxon): void + { + $this->client->addRequestData('menuTaxon', $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin')); + } + /** * @When I specify its :field as :value * @When I :field it :value * @When I set its :field as :value * @When I define its :field as :value + * @When I do not specify its :field */ - public function iSpecifyItsAs(string $field, string $value): void + public function iSpecifyItsAs(string $field, string $value = ''): void { $this->client->addRequestData($field, $value); } /** * @When I choose :currency as the base currency + * @When I do not choose base currency */ - public function iChooseAsTheBaseCurrency(CurrencyInterface $currency): void + public function iChooseAsTheBaseCurrency(?CurrencyInterface $currency = null): void { - $this->client->addRequestData('baseCurrency', $this->iriConverter->getIriFromItemInSection($currency, 'admin')); + $this->client->addRequestData( + 'baseCurrency', + null === $currency ? $currency : $this->sectionAwareIriConverter->getIriFromResourceInSection($currency, 'admin'), + ); + } + + /** + * @When I allow for paying in :currency + */ + public function iAllowToPayingForThisChannel(CurrencyInterface $currency): void + { + $this->client->addRequestData('currencies', [$this->iriConverter->getIriFromResource($currency)]); + } + + /** + * @When I select the :zone as default tax zone + */ + public function iSelectDefaultTaxZone(ZoneInterface $zone): void + { + $this->client->addRequestData('defaultTaxZone', $this->iriConverter->getIriFromResource($zone)); + } + + /** + * @When I remove its default tax zone + */ + public function iRemoveItsDefaultTaxZone(): void + { + $this->client->addRequestData('defaultTaxZone', null); + } + + /** + * @When I make it available in :locale + */ + public function iMakeItAvailableInLocale(LocaleInterface $locale): void + { + $this->client->addRequestData('locales', [$this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin')]); + } + + /** + * @When I make it available only in :locale + */ + public function iMakeItAvailableOnlyInLocale(LocaleInterface $locale): void + { + $this->client->replaceRequestData('locales', [$this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin')]); } /** * @When I choose :locale as a default locale + * @When I do not choose default locale */ - public function iChooseAsADefaultLocale(LocaleInterface $locale): void + public function iChooseAsADefaultLocale(?LocaleInterface $locale = null): void { - $this->client->addRequestData('defaultLocale', $this->iriConverter->getIriFromItemInSection($locale, 'admin')); + $this->client->addRequestData( + 'defaultLocale', + null === $locale ? $locale : $this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin'), + ); } /** @@ -102,8 +196,8 @@ final class ManagingChannelsContext implements Context public function iChooseAndAsOperatingCountries(CountryInterface $country, CountryInterface $otherCountry): void { $this->client->addRequestData('countries', [ - $this->iriConverter->getIriFromItemInSection($country, 'admin'), - $this->iriConverter->getIriFromItemInSection($otherCountry, 'admin'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($country, 'admin'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($otherCountry, 'admin'), ]); } @@ -128,7 +222,7 @@ final class ManagingChannelsContext implements Context */ public function iSpecifyMenuTaxonAs(TaxonInterface $taxon): void { - $this->client->addRequestData('menuTaxon', $this->iriConverter->getIriFromItemInSection($taxon, 'admin')); + $this->client->addRequestData('menuTaxon', $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin')); } /** @@ -147,6 +241,44 @@ final class ManagingChannelsContext implements Context $this->shopBillingData['taxId'] = $taxId; } + /** + * @When /^I specify shop billing data for (this channel) as "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)" tax ID and ("([^"]+)" country)$/ + */ + public function iSpecifyShopBillingDataAs( + ChannelInterface $channel, + string $company, + string $street, + string $postcode, + string $city, + string $taxId, + CountryInterface $country, + ): void { + $shopBillingDataId = $this->iriConverter->getIriFromResource($channel->getShopBillingData()); + + $this->client->addRequestData( + 'shopBillingData', + [ + '@id' => $shopBillingDataId, + 'company' => $company, + 'street' => $street, + 'postcode' => $postcode, + 'city' => $city, + 'countryCode' => $country->getCode(), + 'taxId' => $taxId, + ], + ); + } + + /** + * @When /^I specify new country code for (this channel) as "([^"]+)"$/ + */ + public function iSpecifyNewCountryCodeForThisChannelAs(ChannelInterface $channel, string $code): void + { + $shopBillingDataId = $this->iriConverter->getIriFromResource($channel->getShopBillingData()); + + $this->client->addRequestData('shopBillingData', ['@id' => $shopBillingDataId, 'countryCode' => $code]); + } + /** * @When I specify shop billing address as :street, :postcode :city, :country */ @@ -162,6 +294,15 @@ final class ManagingChannelsContext implements Context $this->shopBillingData['countryCode'] = $country->getCode(); } + /** + * @Then I save it + */ + public function iSaveIt(): void + { + $this->iAddIt(); + $this->client->update(); + } + /** * @When I select the :taxCalculationStrategy as tax calculation strategy */ @@ -172,6 +313,7 @@ final class ManagingChannelsContext implements Context /** * @When I add it + * @When I try to add it */ public function iAddIt(): void { @@ -205,11 +347,25 @@ final class ManagingChannelsContext implements Context } /** - * @When I save my changes + * @When /^I (enable|disable) showing the lowest price of discounted products$/ */ - public function iSaveMyChanges(): void + public function iEnableShowingTheLowestPriceOfDiscountedProducts(string $visible): void { - $this->client->update(); + $this->client->addRequestData( + 'channelPriceHistoryConfig', + ['lowestPriceForDiscountedProductsVisible' => $visible === 'enable'], + ); + } + + /** + * @When /^I specify (-?\d+) days as the lowest price for discounted products checking period$/ + */ + public function iSpecifyDaysAsTheLowestPriceForDiscountedProductsCheckingPeriod(int $days): void + { + $this->client->addRequestData( + 'channelPriceHistoryConfig', + ['lowestPriceForDiscountedProductsCheckingPeriod' => $days], + ); } /** @@ -218,8 +374,8 @@ final class ManagingChannelsContext implements Context public function iShouldBeNotifiedThatItHasBeenSuccessfullyCreated(): void { Assert::true( - $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), - 'Channel could not be created', + $this->responseChecker->isCreationSuccessful($response = $this->client->getLastResponse()), + 'Channel could not be created: ' . $response->getContent(), ); } @@ -242,11 +398,79 @@ final class ManagingChannelsContext implements Context { Assert::same( $this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'menuTaxon'), - $this->iriConverter->getIriFromItemInSection($taxon, 'admin'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin'), sprintf('Channel %s does not have %s menu taxon', $channel->getName(), $taxon->getName()), ); } + /** + * @Then I should not be able to edit its code + */ + public function iShouldNotBeAbleToEditItsCode(): void + { + $this->client->updateRequestData(['code' => 'NEW_CODE']); + + Assert::false($this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE')); + } + + /** + * @Then the base currency field should be disabled + * @Then I should not be able to edit its base currency + */ + public function theBaseCurrencyFieldShouldBeDisabled(): void + { + $this->client->updateRequestData(['baseCurrency' => 'PLN']); + + Assert::false($this->responseChecker->hasValue($this->client->update(), 'baseCurrency', 'PLN')); + } + + /** + * @Then /^(this channel) name should be "([^"]*)"$/ + */ + public function thisChannelNameShouldBe(ChannelInterface $channel, string $name): void + { + Assert::true( + $this->responseChecker->hasValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'name', $name), + sprintf('Its Channel does not have name %s.', $name), + ); + } + + /** + * @Then the :channel channel should no longer exist in the registry + */ + public function theChannelShouldNoLongerExistInTheRegistry(string $name): void + { + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::CHANNELS), 'name', $name), + sprintf('Channel with name %s exists', $name), + ); + } + + /** + * @Then I should be notified that it has been successfully deleted + */ + public function iShouldBeNotifiedThatChannelHasBeenDeleted(): void + { + Assert::true($this->responseChecker->isDeletionSuccessful($this->client->getLastResponse())); + } + + /** + * @Then /^(this channel) menu (taxon should be "([^"]+)")$/ + */ + public function thisChannelMenuTaxonShouldBe(ChannelInterface $channel, TaxonInterface $taxon): void + { + Assert::true( + $this->responseChecker->hasValue( + $this->client->show( + Resources::CHANNELS, + $channel->getCode(), + ), + 'menuTaxon', + $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin'), + ), + ); + } + /** * @Then I should see :count channels in the list */ @@ -268,13 +492,223 @@ final class ManagingChannelsContext implements Context } /** - * @Then I should be notified that it has been successfully edited + * @Then I should be notified that it cannot be deleted */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void + public function iShouldBeNotifiedThatItCannotBeDeleted(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'The channel cannot be deleted. At least one enabled channel is required.', + ); + } + + /** + * @Then I should be notified that at least one channel has to be defined + */ + public function iShouldBeNotifiedThatAtLeastOneChannelHasToBeDefined(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Must have at least one enabled entity', + ); + } + + /** + * @Then channel with name :channel should still be enabled + * @Then /^(this channel) should be enabled$/ + */ + public function channelWithNameShouldStillBeEnabled(ChannelInterface $channel): void { Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Channel could not be edited', + $this->responseChecker->hasValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'enabled', true), + sprintf('Channel with name %s does not exists', $channel->getName()), + ); + } + + /** + * @Then this channel should still be named :channel + */ + public function thisChannelShouldStillBeNamed(ChannelInterface $channel): void + { + Assert::true( + $this->responseChecker->hasValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'name', $channel->getName()), + sprintf('Channel with name %s does not exists', $channel->getName()), + ); + } + + /** + * @Then paying in :currency should be possible for the :channel channel + */ + public function payingInCurrencyShouldBePossibleForTheChannel(CurrencyInterface $currency, ChannelInterface $channel): void + { + $currencies = $this->responseChecker->getValue( + $this->client->show(Resources::CHANNELS, $channel->getCode()), + 'currencies', + ); + + Assert::true(in_array($this->sectionAwareIriConverter->getIriFromResourceInSection($currency, 'admin'), $currencies)); + } + + /** + * @Then channel :channel should not have default tax zone + */ + public function channelShouldNotHaveDefaultTaxZone(ChannelInterface $channel): void + { + Assert::same( + $this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'defaultTaxZone'), + null, + sprintf('Channel %s has default tax zone', $channel->getName()), + ); + } + + /** + * @Then the default tax zone for the :channel channel should be :zone + */ + public function theDefaultTaxZoneForTheChannelShouldBe(ChannelInterface $channel, ZoneInterface $zone): void + { + Assert::same( + $this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'defaultTaxZone'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($zone, 'admin'), + sprintf('Channel %s does not have %s default tax zone', $channel->getName(), $zone), + ); + } + + /** + * @Then the channel :channel should be available in :locale + */ + public function theChannelShouldBeAvailableIn(ChannelInterface $channel, LocaleInterface $locale): void + { + $locales = $this->responseChecker->getValue( + $this->client->show(Resources::CHANNELS, $channel->getCode()), + 'locales', + ); + + Assert::true(in_array($this->sectionAwareIriConverter->getIriFromResourceInSection($locale, 'admin'), $locales)); + } + + /** + * @Then I should be notified that the default locale has to be enabled + */ + public function iShouldBeNotifiedThatTheDefaultLocaleHasToBeEnabled(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'defaultLocale: Default locale has to be enabled.', + ); + } + + /** + * @Then /^(this channel) should still be in the registry$/ + */ + public function thisChannelShouldStillBeInTheRegistry(ChannelInterface $channel): void + { + Assert::true( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::CHANNELS), 'code', $channel->getCode()), + sprintf('Channel with code %s does not exists', $channel->getCode()), + ); + } + + /** + * @Then the tax calculation strategy for the :channel channel should be :taxCalculationStrategy + */ + public function theTaxCalculationStrategyForTheChannelShouldBe( + ChannelInterface $channel, + string $taxCalculationStrategy, + ): void { + Assert::same( + $this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'taxCalculationStrategy'), + StringInflector::nameToLowercaseCode($taxCalculationStrategy), + sprintf('Channel %s does not have %s tax calculation strategy', $channel->getName(), $taxCalculationStrategy), + ); + } + + /** + * @Then /^(this channel) should be disabled$/ + */ + public function thisChannelShouldBeDisabled(ChannelInterface $channel): void + { + Assert::same( + $this->responseChecker->getValue($this->client->show(Resources::CHANNELS, $channel->getCode()), 'enabled'), + false, + sprintf('Channel %s is enabled', $channel->getName()), + ); + } + + /** + * @Then I should be notified that :element is required + */ + public function iShouldBeNotifiedThatIsRequired(string $element): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('%s: Please enter channel %s.', $element, $element), + ); + } + + /** + * @Then I should be notified that base currency is required + */ + public function iShouldBeNotifiedThatBaseCurrencyIsRequired(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Expected IRI or nested document for attribute "baseCurrency", "NULL" given.', + ); + } + + /** + * @Then I should be notified that default locale is required + */ + public function iShouldBeNotifiedThatDefaultLocaleIsRequired(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Expected IRI or nested document for attribute "defaultLocale", "NULL" given.', + ); + } + + /** + * @Then channel with :element :value should not be added + */ + public function channelWithShouldNotBeAdded(string $element, string $value): void + { + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::CHANNELS), $element, $value), + sprintf('Channel with %s: %s exists', $element, $value), + ); + } + + /** + * @Then I should be notified that channel with this code already exists + */ + public function iShouldBeNotifiedThatChannelWithThisCodeAlreadyExists(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'code: Channel code has to be unique.', + ); + } + + /** + * @Then there should still be only one channel with :element :value + */ + public function thereShouldStillBeOnlyOneChannelWithCode(string $element, string $value): void + { + Assert::same( + count($this->responseChecker->getCollectionItemsWithValue($this->client->index(Resources::CHANNELS), $element, $value)), + 1, + sprintf('There is more than one channel with %s: %s', $element, $value), + ); + } + + /** + * @Then I should be notified that it is not a valid country + */ + public function iShouldBeNotifiedThatItIsNotAValidCountryCode(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'countryCode: This value is not a valid country.', ); } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingCountriesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingCountriesContext.php index 4994870b88..24dfa60b92 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingCountriesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingCountriesContext.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; @@ -47,7 +47,15 @@ final class ManagingCountriesContext implements Context */ public function iChoose(string $countryName): void { - $this->client->addRequestData('code', $this->getCountryCodeByName($countryName)); + $this->iSpecifyTheCountryCodeAs($this->getCountryCodeByName($countryName)); + } + + /** + * @When I specify the country code as :code + */ + public function iSpecifyTheCountryCodeAs(string $code): void + { + $this->client->addRequestData('code', $code); } /** @@ -85,15 +93,6 @@ final class ManagingCountriesContext implements Context $this->client->addRequestData('enabled', false); } - /** - * @When I save my changes - * @When I try to save changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @When I name the province :provinceName */ @@ -154,10 +153,11 @@ final class ManagingCountriesContext implements Context } /** + * @When I do not specify the country code * @When I do not specify the province code * @When I do not name the province */ - public function iDoNotSpecifyTheProvince(): void + public function iDoNotSpecifyTheField(): void { // Intentionally left blank } @@ -245,29 +245,20 @@ final class ManagingCountriesContext implements Context Assert::same($this->responseChecker->getError($response), 'code: Country ISO code must be unique.'); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Country could not be edited', - ); - } - /** * @Then /^(this country) should be (enabled|disabled)$/ */ - public function thisCountryShouldBeDisabled(CountryInterface $country, string $enabled): void + public function thisCountryShouldBe(CountryInterface $country, string $state): void { + $isEnabled = 'enabled' === $state; + Assert::true( $this->responseChecker->hasValue( $this->client->show(Resources::COUNTRIES, $country->getCode()), 'enabled', - $enabled === 'enabled', + $isEnabled, ), - 'Country is not disabled', + sprintf('Country is not %s', $isEnabled ? 'enabled' : 'disabled'), ); } @@ -345,6 +336,17 @@ final class ManagingCountriesContext implements Context ); } + /** + * @Then /^I should be notified that the country code is (required|invalid)$/ + */ + public function iShouldBeNotifiedThatTheCountryCodeIsRequired(string $constraint): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + $constraint === 'required' ? 'Please enter country ISO code.' : 'Country ISO code is invalid.', + ); + } + /** * @Then I should be notified that name of the province is required */ @@ -379,13 +381,14 @@ final class ManagingCountriesContext implements Context return $countryList[$countryName]; } + /** @return iterable */ private function getProvincesOfCountry(CountryInterface $country): iterable { $response = $this->client->show(Resources::COUNTRIES, $country->getCode()); $countryFromResponse = $this->responseChecker->getResponseContent($response); foreach ($countryFromResponse['provinces'] as $provinceFromResponse) { - yield $this->iriConverter->getItemFromIri($provinceFromResponse); + yield $this->iriConverter->getResourceFromIri($provinceFromResponse); } } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingCustomerGroupsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingCustomerGroupsContext.php index 456a69f1bb..b5717dda82 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingCustomerGroupsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingCustomerGroupsContext.php @@ -72,14 +72,6 @@ final class ManagingCustomerGroupsContext implements Context $this->client->buildUpdateRequest(Resources::CUSTOMER_GROUPS, $customerGroup->getCode()); } - /** - * @When I (try to) save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @When I browse customer groups * @When I want to browse customer groups @@ -203,17 +195,6 @@ final class ManagingCustomerGroupsContext implements Context ); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Customer group could not be edited', - ); - } - /** * @Then I should be notified that it has been successfully deleted */ diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingCustomersContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingCustomersContext.php new file mode 100644 index 0000000000..e725275f5d --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingCustomersContext.php @@ -0,0 +1,635 @@ + 'asc', 'descending' => 'desc']; + + public function __construct( + private ApiClientInterface $client, + private ResponseCheckerInterface $responseChecker, + private IriConverterInterface $iriConverter, + private SharedStorageInterface $sharedStorage, + ) { + } + + /** + * @When I want to create a new customer + * @When I want to create a new customer account + */ + public function iWantToCreateANewCustomer(): void + { + $this->client->buildCreateRequest(Resources::CUSTOMERS); + } + + /** + * @When /^I want to edit (this customer)$/ + * @When I want to enable :customer + * @When I want to disable :customer + * @When I want to verify :customer + */ + public function iWantToEditThisCustomer(CustomerInterface $customer): void + { + $this->client->buildUpdateRequest(Resources::CUSTOMERS, (string) $customer->getId()); + } + + /** + * @When I browse orders of a customer :customer + */ + public function iBrowseOrdersOfACustomer(CustomerInterface $customer): void + { + $this->client->index(Resources::ORDERS); + $this->client->addFilter('customer.id', $customer->getId()); + $this->client->filter(); + } + + /** + * @When I specify their email as :email + * @When I do not specify their email + * @When I change their email to :email + * @When I remove its email + */ + public function iChangeTheirEmailTo(?string $email = null): void + { + $this->client->addRequestData('email', (string) $email); + } + + /** + * @When /^I specify (?:their|his) first name as "([^"]*)"$/ + * @When I remove its first name + */ + public function iSpecifyTheirFirstNameAs(?string $name = null): void + { + $this->client->addRequestData('firstName', $name); + } + + /** + * @When /^I specify (?:their|his) last name as "([^"]*)"$/ + * @When I remove its last name + */ + public function iSpecifyTheirLastNameAs(?string $name = null): void + { + $this->client->addRequestData('lastName', $name); + } + + /** + * @When I specify its birthday as :birthday + */ + public function iSpecifyItsBirthdayAs(string $birthday): void + { + $this->client->addRequestData('birthday', $birthday); + } + + /** + * @When I select :gender as its gender + */ + public function iSelectGender(string $gender): void + { + $this->client->addRequestData('gender', strtolower(substr($gender, 0, 1))); + } + + /** + * @When I select :customerGroup as their group + */ + public function iSelectGroup(CustomerGroupInterface $customerGroup): void + { + $this->client->addRequestData('group', $this->iriConverter->getIriFromItem($customerGroup)); + } + + /** + * @When I make them subscribed to the newsletter + */ + public function iMakeThemSubscribedToTheNewsletter(): void + { + $this->client->addRequestData('subscribedToNewsletter', true); + } + + /** + * @When I choose create account option + */ + public function iChooseCreateAccountOption(): void + { + $this->client->addRequestData('user', []); + } + + /** + * @When I specify their password as :password + */ + public function iSpecifyItsPasswordAs(string $password): void + { + $this->client->addRequestData('user', [ + 'plainPassword' => $password, + ]); + } + + /** + * @When /^I (enable|disable) their account$/ + */ + public function iEnableIt(string $toggleAction): void + { + $this->client->addRequestData('user', [ + 'enabled' => 'enable' === $toggleAction, + ]); + } + + /** + * @When I verify it + */ + public function iVerifyIt(): void + { + $this->client->addRequestData('user', [ + 'verified' => true, + ]); + } + + /** + * @When I (try to) add them + */ + public function iAddIt(): void + { + $this->client->create(); + } + + /** + * @When I want to see all customers in store + */ + public function iWantToSeeAllCustomersInStore(): void + { + $this->client->index(Resources::CUSTOMERS); + } + + /** + * @When I view details of the customer :customer + * @When /^I view (their) details$/ + */ + public function iViewDetailsOfTheCustomer(CustomerInterface $customer): void + { + $this->client->show(Resources::CUSTOMERS, (string) $customer->getId()); + } + + /** + * @When I filter by group :groupName + * @When I filter by groups :firstGroup and :secondGroup + */ + public function iFilterByGroup(string ...$groupsNames): void + { + foreach ($groupsNames as $groupName) { + $this->client->addFilter('group.name[]', $groupName); + } + $this->client->filter(); + } + + /** + * @When I sort the orders :sortType by channel + */ + public function iSortThemBy(string $sortType = 'ascending'): void + { + $this->client->sort([ + 'channel.code' => self::SORT_TYPES[$sortType], + ]); + } + + /** + * @When I change the password of user :customer to :newPassword + */ + public function iChangeThePasswordOfUserTo(CustomerInterface $customer, string $newPassword): void + { + $this->iWantToEditThisCustomer($customer); + $this->iSpecifyItsPasswordAs($newPassword); + $this->client->update(); + } + + /** + * @When I delete the account of :shopUser user + */ + public function iDeleteAccount(ShopUserInterface $shopUser): void + { + $this->sharedStorage->set('customer', $shopUser->getCustomer()); + $this->client->delete(sprintf('customers/%s', $shopUser->getCustomer()->getId()), 'user'); + } + + /** + * @When I do not specify any information + * @When I do not choose create account option + */ + public function intentionallyLeftEmpty(): void + { + } + + /** + * @Then I should be notified that it has been successfully created + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyCreated(): void + { + Assert::true( + $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), + 'Customer could not be created', + ); + } + + /** + * @Then I should be notified that :element is required + */ + public function iShouldBeNotifiedThatIsRequired(string $element): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('Please enter your %s.', $element), + ); + } + + /** + * @Then I should be notified that email must be unique + */ + public function iShouldBeNotifiedThatEmailMustBeUnique(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'email: This email is already used.', + ); + } + + /** + * @Then /^I should be notified that ([^"]+) should be ([^"]+)$/ + */ + public function iShouldBeNotifiedThatTheElementShouldBe(string $elementName, string $validationMessage): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('%s must be %s.', ucfirst($elementName), $validationMessage), + ); + } + + /** + * @Then I should be notified that email is not valid + */ + public function iShouldBeNotifiedThatEmailIsNotValid(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'This email is invalid.', + ); + } + + /** + * @Then the customer :customer should appear in the store + * @Then the customer :customer should still have this email + */ + public function theCustomerShouldAppearInTheStore(CustomerInterface $customer): void + { + Assert::true( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::CUSTOMERS), 'email', $customer->getEmail()), + sprintf('Customer with email %s does not exist', $customer->getEmail()), + ); + } + + /** + * @Then the customer :customer should have an account created + * @Then /^(this customer) should have an account created$/ + */ + public function theyShouldHaveAnAccountCreated(CustomerInterface $customer): void + { + Assert::notNull( + $customer->getUser()->getPassword(), + 'Customer should have an account, but they do not.', + ); + } + + /** + * @Then I should see :count customers in the list + * @Then I should see a single customer on the list + */ + public function iShouldSeeZonesInTheList(int $count = 1): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $count); + } + + /** + * @Then I should see the customer :email in the list + */ + public function iShouldSeeTheCustomerInTheList(string $email): void + { + Assert::true( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::CUSTOMERS), 'email', $email), + sprintf('There is no customer with email "%s"', $email), + ); + } + + /** + * @Then I should see a single order in the list + */ + public function iShouldSeeASingleOrderInTheList(): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), 1); + } + + /** + * @Then their name should be :name + */ + public function theirNameShouldBe(string $name): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'fullName', $name)); + } + + /** + * @Then he should be registered since :registrationDate + */ + public function hisRegistrationDateShouldBe(string $registrationDate): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'createdAt', $registrationDate)); + } + + /** + * @Then their email should be :email + */ + public function theirEmailShouldBe(string $email): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'email', $email)); + } + + /** + * @Then their phone number should be :phoneNumber + */ + public function theirPhoneNumberShouldBe(string $phoneNumber): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'phoneNumber', $phoneNumber)); + } + + /** + * @Then their default address should be :firstName :lastName, :street, :postcode :city, :country + */ + public function theirSDefaultAddressShouldBe( + string $firstName, + string $lastName, + string $street, + string $postcode, + string $city, + CountryInterface $country, + ): void { + $this->client->showByIri($this->responseChecker->getValue($this->client->getLastResponse(), 'defaultAddress')); + + Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'firstName'), $firstName); + Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'lastName'), $lastName); + Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'street'), $street); + Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'postcode'), $postcode); + Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'city'), $city); + Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'countryCode'), $country->getCode()); + } + + /** + * @Then the province in the default address should be :provinceName + */ + public function theProvinceInTheDefaultAddressShouldBe(string $provinceName): void + { + $this->client->showByIri($this->responseChecker->getValue($this->client->getLastResponse(), 'defaultAddress')); + Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'provinceName'), $provinceName); + } + + /** + * @Then I should see information about no existing account for this customer + * @Then I should not see information about email verification + */ + public function iShouldSeeInformationAboutNoExistingAccountForThisCustomer(): void + { + Assert::null($this->responseChecker->getValue($this->client->getLastResponse(), 'user')); + } + + /** + * @Then I should see that this customer has verified the email + */ + public function iShouldSeeThatThisCustomerHasVerifiedTheEmail(): void + { + $user = $this->responseChecker->getValue($this->client->getLastResponse(), 'user'); + Assert::true($user['verified']); + } + + /** + * @Then I should see the order with number :orderNumber in the list + */ + public function iShouldSeeTheOrderWithNumberInTheList(string $orderNumber): void + { + Assert::true( + $this->responseChecker->hasItemWithValue( + $this->client->getLastResponse(), + 'number', + $orderNumber, + ), + ); + } + + /** + * @Then I should be notified that the password must be at least :amountOfCharacters characters long + */ + public function iShouldBeNotifiedThatThePasswordMustBeAtLeastCharactersLong(int $amountOfCharacters): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('Password must be at least %d characters long.', $amountOfCharacters), + ); + } + + /** + * @Then I should not see the order with number :orderNumber in the list + */ + public function iShouldNotSeeASingleOrderFromCustomer(string $orderNumber): void + { + Assert::false( + $this->responseChecker->hasItemWithValue( + $this->client->getLastResponse(), + 'number', + $orderNumber, + ), + ); + } + + /** + * @Then /^(this customer) should be (enabled|disabled)$/ + */ + public function thisCustomerShouldBeEnabled(CustomerInterface $customer, string $toggleAction): void + { + $user = $this->responseChecker->getValue( + $this->client->show(Resources::CUSTOMERS, (string) $customer->getId()), + 'user', + ); + Assert::same($user['enabled'], 'enabled' === $toggleAction); + } + + /** + * @Then /^(this customer) should be verified$/ + */ + public function thisCustomerShouldBeVerified(CustomerInterface $customer): void + { + $user = $this->responseChecker->getValue( + $this->client->show(Resources::CUSTOMERS, (string) $customer->getId()), + 'user', + ); + Assert::true($user['verified']); + } + + /** + * @Then there should still be only one customer with email :email + */ + public function thereShouldStillBeOnlyOneCustomerWithEmail(string $email): void + { + Assert::count( + $this->responseChecker->getCollectionItemsWithValue($this->client->index(Resources::CUSTOMERS), 'email', $email), + 1, + sprintf('There is more than one customer with email %s', $email), + ); + } + + /** + * @Then /^(this customer) should have an empty first name$/ + * @Then the customer :customer should still have an empty first name + */ + public function theCustomerShouldStillHaveAnEmptyFirstName(CustomerInterface $customer): void + { + Assert::null( + $this->responseChecker->getValue( + $this->client->show(Resources::CUSTOMERS, (string) $customer->getId()), + 'firstName', + ), + ); + } + + /** + * @Then /^(this customer) should have an empty last name$/ + * @Then the customer :customer should still have an empty last name + */ + public function theCustomerShouldStillHaveAnEmptyLastName(CustomerInterface $customer): void + { + Assert::null( + $this->responseChecker->getValue( + $this->client->show(Resources::CUSTOMERS, (string) $customer->getId()), + 'lastName', + ), + ); + } + + /** + * @Then the customer with email :email should not appear in the store + */ + public function theCustomerShouldNotAppearInTheStore(string $email): void + { + Assert::false( + $this->responseChecker->hasItemWithValue( + $this->client->index(Resources::CUSTOMERS), + 'email', + $email, + ), + ); + } + + /** + * @Then /^(this customer) with name "([^"]*)" should appear in the store$/ + */ + public function theCustomerWithNameShouldAppearInTheStore(CustomerInterface $customer, string $name): void + { + Assert::true( + $this->responseChecker->hasValue( + $this->client->show(Resources::CUSTOMERS, (string) $customer->getId()), + 'fullName', + $name, + ), + ); + } + + /** + * @Then this customer should be subscribed to the newsletter + * @Then I should see that this customer is subscribed to the newsletter + */ + public function thisCustomerShouldBeSubscribedToTheNewsletter(): void + { + Assert::true( + $this->responseChecker->getValue( + $this->client->getLastResponse(), + 'subscribedToNewsletter', + ), + ); + } + + /** + * @Then this customer should have :customerGroup as their group + */ + public function thisCustomerShouldHaveAsTheirGroup(CustomerGroupInterface $customerGroup): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'group'), + $this->iriConverter->getIriFromItem($customerGroup), + ); + } + + /** + * @Then the customer with this email should still exist + */ + public function customerShouldStillExist(): void + { + /** @var CustomerInterface $customer */ + $customer = $this->sharedStorage->get('customer'); + + $this->client->show(Resources::CUSTOMERS, (string) $customer->getId()); + + Assert::same($this->client->getLastResponse()->getStatusCode(), 200); + Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'email'), $customer->getEmail()); + } + + /** + * @Then I should not see create account option + * @Then I should still be on the customer creation page + * @Then I should be able to specify their password + * @Then I should not be able to specify their password + * @Then I should be able to select create account option + * @Then I should not be able to select create account option + */ + public function intentionallyLeftBlank(): void + { + } + + /** + * @Then the user account should be deleted + */ + public function accountShouldBeDeleted(): void + { + /** @var CustomerInterface $customer */ + $customer = $this->sharedStorage->get('customer'); + + $response = $this->client->show(Resources::CUSTOMERS, (string) $customer->getId()); + + Assert::null($this->responseChecker->getValue($response, 'user')); + } + + /** + * @Then I should not be able to delete it again + */ + public function iShouldNotBeAbleToDeleteCustomerAgain(): void + { + $customer = $this->sharedStorage->get('customer'); + $this->client->delete(sprintf('customer/%s', $customer->getId()), 'user'); + + Assert::same($this->client->getLastResponse()->getStatusCode(), 404); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingExchangeRatesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingExchangeRatesContext.php index 8b5270d2fd..5af02ab4a4 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingExchangeRatesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingExchangeRatesContext.php @@ -110,14 +110,6 @@ final class ManagingExchangeRatesContext implements Context $this->client->updateRequestData(['ratio' => $ratio]); } - /** - * @When I save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @When /^I delete the (exchange rate between "[^"]+" and "[^"]+")$/ */ @@ -319,17 +311,6 @@ final class ManagingExchangeRatesContext implements Context ); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Exchange rate could not be edited', - ); - } - /** * @Then I should be notified that it has been successfully deleted */ diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingLocalesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingLocalesContext.php index 260aadb4f9..ea6d909b6a 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingLocalesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingLocalesContext.php @@ -17,6 +17,7 @@ use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Symfony\Component\HttpFoundation\Response; use Webmozart\Assert\Assert; final class ManagingLocalesContext implements Context @@ -53,6 +54,14 @@ final class ManagingLocalesContext implements Context $this->client->create(); } + /** + * @When I remove :localeCode locale + */ + public function iRemoveLocale(string $localeCode): void + { + $this->client->delete(Resources::LOCALES, $localeCode); + } + /** * @Then I should be notified that it has been successfully created */ @@ -115,4 +124,46 @@ final class ManagingLocalesContext implements Context ); Assert::same($this->responseChecker->getError($response), 'code: This value is not a valid locale code.'); } + + /** + * @Then I should be informed that locale :localeCode has been deleted + */ + public function iShouldBeInformedThatLocaleHasBeenDeleted(string $localeCode): void + { + Assert::same($this->client->getLastResponse()->getStatusCode(), Response::HTTP_NO_CONTENT); + } + + /** + * @Then only the :localeCode locale should be present in the system + */ + public function onlyTheLocaleShouldBePresentInTheSystem(string $localeCode): void + { + $response = $this->client->index(Resources::LOCALES); + Assert::true($this->responseChecker->countCollectionItems($response) === 1); + Assert::true( + $this->responseChecker->hasItemWithValue($response, 'code', $localeCode), + sprintf('There is no locale with code "%s"', $localeCode), + ); + } + + /** + * @Then I should be informed that locale :localeCode is in use and cannot be deleted + */ + public function iShouldBeInformedThatLocaleIsInUseAndCannotBeDeleted(string $localeCode): void + { + Assert::same($this->client->getLastResponse()->getStatusCode(), Response::HTTP_UNPROCESSABLE_ENTITY); + Assert::same($this->responseChecker->getError($this->client->getLastResponse()), sprintf('Locale "%s" is used.', $localeCode)); + } + + /** + * @Then the :localeCode locale should be still present in the system + */ + public function theLocaleShouldBeStillPresentInTheSystem(string $localeCode): void + { + $response = $this->client->index(Resources::LOCALES); + Assert::true( + $this->responseChecker->hasItemWithValue($response, 'code', $localeCode), + sprintf('There is no locale with code "%s"', $localeCode), + ); + } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php index 680ead101e..1f43b5761f 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingOrdersContext.php @@ -13,19 +13,31 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Context\Api\Subresources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Behat\Service\SecurityServiceInterface; +use Sylius\Behat\Service\SharedSecurityServiceInterface; use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Component\Core\Model\AdjustmentInterface; use Sylius\Component\Core\Model\AdminUserInterface; +use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; +use Sylius\Component\Core\Model\PaymentMethodInterface; +use Sylius\Component\Core\Model\ShipmentInterface; +use Sylius\Component\Core\Model\ShippingMethodInterface; +use Sylius\Component\Currency\Model\CurrencyInterface; use Sylius\Component\Order\OrderTransitions; use Sylius\Component\Payment\PaymentTransitions; use Sylius\Component\Shipping\ShipmentTransitions; +use Symfony\Component\HttpFoundation\Request as HttpRequest; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Intl\Countries; use Webmozart\Assert\Assert; final class ManagingOrdersContext implements Context @@ -36,6 +48,8 @@ final class ManagingOrdersContext implements Context private IriConverterInterface $iriConverter, private SecurityServiceInterface $adminSecurityService, private SharedStorageInterface $sharedStorage, + private SharedSecurityServiceInterface $sharedSecurityService, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, ) { } @@ -45,10 +59,14 @@ final class ManagingOrdersContext implements Context */ public function iSeeTheOrder(OrderInterface $order): void { - $this->client->show(Resources::ORDERS, $order->getTokenValue()); + $response = $this->client->show(Resources::ORDERS, $order->getTokenValue()); + Assert::same($this->responseChecker->getValue($response, '@id'), $this->sectionAwareIriConverter->getIriFromResourceInSection($order, 'admin')); + + $this->sharedStorage->set('order', $order); } /** + * @Given I am browsing orders * @When I browse orders */ public function iBrowseOrders(): void @@ -56,6 +74,170 @@ final class ManagingOrdersContext implements Context $this->client->index(Resources::ORDERS); } + /** + * @When I browse order's :order history + */ + public function iBrowseOrderHistory(OrderInterface $order): void + { + $this->iSeeTheOrder($order); + } + + /** + * @When I filter + */ + public function iFilter(): void + { + $this->client->filter(); + } + + /** + * @When I choose :channel as a channel filter + */ + public function iChooseChannelAsAChannelFilter(ChannelInterface $channel): void + { + $this->client->addFilter('channel.code', $channel->getCode()); + } + + /** + * @When I specify filter date from as :dateTime + */ + public function iSpecifyFilterDateFromAs(string $dateTime): void + { + $this->client->addFilter('checkoutCompletedAt[after]', $dateTime); + } + + /** + * @When specify its tracking code as :trackingCode + */ + public function specifyItsTrackingCodeAs(string $trackingCode): void + { + $shipment = $this->sharedStorage->get('order')->getShipments()->first(); + + $this->client->buildUpdateRequest( + Resources::SHIPMENTS, + (string) $shipment->getId(), + ); + + $this->client->addRequestData('tracking', $trackingCode); + $this->client->update(); + } + + /** + * @When /^I try to view the summary of the (customer's latest cart)$/ + */ + public function iTryToViewTheSummaryOfTheCustomersLatestCart(OrderInterface $cart): void + { + $this->client->show(Resources::ORDERS, $cart->getTokenValue()); + } + + /** + * @When I specify filter date to as :dateTime + */ + public function iSpecifyFilterDateToAs(string $dateTime): void + { + $this->client->addFilter('checkoutCompletedAt[before]', $dateTime); + } + + /** + * @When I resend the order confirmation email + */ + public function iResendTheOrderConfirmationEmail(): void + { + $this->client->customItemAction( + Resources::ORDERS, + $this->sharedStorage->get('order')->getTokenValue(), + HttpRequest::METHOD_POST, + 'resend-confirmation-email', + ); + } + + /** + * @When I filter by product :productName + * @When I filter by products :firstProduct and :secondProduct + */ + public function iFilterByProduct(string ...$productNames): void + { + foreach ($productNames as $productName) { + $this->client->addFilter('items.productName[]', $productName); + } + + $this->client->filter(); + } + + /** + * @When I resend the shipment confirmation email + */ + public function iResendTheShipmentConfirmationEmail(): void + { + $this->client->customItemAction( + Resources::SHIPMENTS, + (string) $this->sharedStorage->get('order')->getShipments()->last()->getId(), + HttpRequest::METHOD_POST, + 'resend-confirmation-email', + ); + } + + /** + * @When I choose :shippingMethod as a shipping method filter + */ + public function iChooseAsAShippingMethodFilter(ShippingMethodInterface $shippingMethod): void + { + $this->client->addFilter('shipments.method.code', $shippingMethod->getCode()); + } + + /** + * @When I choose :currency as the filter currency + */ + public function iChooseCurrencyAsTheFilterCurrency(CurrencyInterface $currency): void + { + $this->client->addFilter('currencyCode', $currency->getCode()); + } + + /** + * @When I specify filter total being greater than :total + */ + public function iSpecifyFilterTotalBeingGreaterThan(string $total): void + { + if (str_contains($total, '.')) { + $total = str_replace('.', '', $total); + $this->client->addFilter('total[gt]', $total); + + return; + } + + $this->client->addFilter('total[gt]', $total . '00'); + } + + /** + * @When I specify filter total being less than :total + */ + public function iSpecifyFilterTotalBeingLessThan(string $total): void + { + $this->client->addFilter('total[lt]', $total . '00'); + } + + /** + * @When I filter by variant :variantName + * @When I filter by variants :firstVariant and :secondVariant + */ + public function iFilterByVariant(string ...$variantsNames): void + { + foreach ($variantsNames as $variantName) { + $this->client->addFilter('items.variant.translations.name[]', $variantName); + } + + $this->client->filter(); + } + + /** + * @When I switch the way orders are sorted by :fieldName + */ + public function iSwitchSortingBy(string $fieldName): void + { + $this->client->addFilter('order[number]', 'asc'); + $this->client->filter(); + } + /** * @When /^I cancel (this order)$/ */ @@ -85,11 +267,16 @@ final class ManagingOrdersContext implements Context */ public function iShipThisOrder(OrderInterface $order): void { + $shipment = $order->getShipments()->last(); + Assert::notNull($shipment, 'There is no shipment for this order'); + $this->client->applyTransition( Resources::SHIPMENTS, - (string) $order->getShipments()->first()->getId(), + (string) $shipment->getId(), ShipmentTransitions::TRANSITION_SHIP, ); + + $this->sharedStorage->set('shipment', $shipment); } /** @@ -101,6 +288,27 @@ final class ManagingOrdersContext implements Context $this->client->filter(); } + /** + * @When I check :itemName data + */ + public function iCheckData(string $itemName): void + { + /** @var string $lastResponseContent */ + $lastResponseContent = $this->client->getLastResponse()->getContent(); + /** @var array{productName: string}[] $items */ + $items = json_decode($lastResponseContent, true)['items']; + + foreach ($items as $item) { + if ($item['productName'] === $itemName) { + $this->sharedStorage->set('item', $item); + + return; + } + } + + throw new \InvalidArgumentException(sprintf('There is no item with name "%s".', $itemName)); + } + /** * @Then I should see a single order from customer :customer */ @@ -110,18 +318,36 @@ final class ManagingOrdersContext implements Context $this->responseChecker->hasItemWithValue( $this->client->getLastResponse(), 'customer', - $this->iriConverter->getIriFromItem($customer), + $this->iriConverter->getIriFromResource($customer), ), sprintf('There is no order for customer %s', $customer->getEmail()), ); } /** - * @Then I should see a single order in the list + * @Then /^I should be notified that the (order|shipment) confirmation email has been successfully resent to the customer$/ */ - public function iShouldSeeASingleOrderInTheList(): void + public function iShouldBeNotifiedThatTheOrderConfirmationEmailHasBeenSuccessfullyResentToTheCustomer(): void { - Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), 1); + $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()); + } + + /** + * @Then it should( still) have a :state state + */ + public function itShouldHaveState(string $state): void + { + Assert::true($this->responseChecker->hasItemWithValue($this->client->getLastResponse(), 'state', $state)); + Assert::count($this->responseChecker->getCollection($this->client->getLastResponse()), 1); + } + + /** + * @Then I should see a single order in the list + * @Then I should see :number orders in the list + */ + public function iShouldSeeASingleOrderInTheList(int $number = 1): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $number); } /** @@ -150,12 +376,13 @@ final class ManagingOrdersContext implements Context } /** - * @Then it should have shipment in state :state + * @Then /^(it) should have shipment in state "([^"]+)"$/ + * @Then /^(order "[^"]+") should have shipment state "([^"]+)"$/ */ - public function itShouldHaveShipmentState(string $state): void + public function itShouldHaveShipmentState(OrderInterface $order, string $state): void { $shipmentIri = $this->responseChecker->getValue( - $this->client->show(Resources::ORDERS, $this->sharedStorage->get('order')->getTokenValue()), + $this->client->show(Resources::ORDERS, $order->getTokenValue()), 'shipments', )[0]; @@ -193,6 +420,7 @@ final class ManagingOrdersContext implements Context } /** + * @Then the order :order should have order payment state :orderPaymentState * @Then /^(this order) should have order payment state "([^"]+)"$/ */ public function theOrderShouldHavePaymentState(OrderInterface $order, string $paymentState): void @@ -243,6 +471,24 @@ final class ManagingOrdersContext implements Context Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'taxTotal'), $taxTotal); } + /** + * @Then I should not be able to resend the shipment confirmation email + */ + public function iShouldNotBeAbleToResendTheShipmentConfirmationEmail(): void + { + $this->client->customItemAction( + Resources::SHIPMENTS, + (string) $this->sharedStorage->get('order')->getShipments()->last()->getId(), + HttpRequest::METHOD_POST, + 'resend-confirmation-email', + ); + + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Cannot resend shipment confirmation email for shipment in state ready.', + ); + } + /** * @Then /^the order's items total should be ("[^"]+")$/ */ @@ -271,7 +517,7 @@ final class ManagingOrdersContext implements Context $this->iCancelThisOrder($order); Assert::contains( $this->responseChecker->getError($this->client->getLastResponse()), - 'Transition "cancel" cannot be applied', + 'Cannot cancel the order.', ); } @@ -280,8 +526,10 @@ final class ManagingOrdersContext implements Context */ public function theOrdersTotalShouldBe(int $total): void { + $response = $this->client->show(Resources::ORDERS, $this->sharedStorage->get('order')->getTokenValue()); + Assert::same( - $this->responseChecker->getValue($this->client->getLastResponse(), 'total'), + $this->responseChecker->getValue($response, 'total'), $total, ); } @@ -291,12 +539,73 @@ final class ManagingOrdersContext implements Context */ public function theOrdersPromotionTotalShouldBe(int $promotionTotal): void { + $response = $this->client->show(Resources::ORDERS, $this->sharedStorage->get('order')->getTokenValue()); + Assert::same( - $this->responseChecker->getValue($this->client->getLastResponse(), 'orderPromotionTotal'), + $this->responseChecker->getValue($response, 'orderPromotionTotal'), $promotionTotal, ); } + /** + * @Then the order's promotion discount should be :promotionAmount from :promotionName promotion + */ + public function theOrdersPromotionDiscountShouldBeFromPromotion(string $promotionAmount, string $promotionName): void + { + $this->responseChecker->hasItemWithValues( + $this->getAdjustmentsResponseForOrder(true), + [ + 'type' => AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT, + 'label' => $promotionName, + 'amount' => $this->getTotalAsInt($promotionAmount), + ], + ); + } + + /** + * @Then the order's shipping promotion should be :promotionAmount + */ + public function theOrdersShippingPromotionDiscountShouldBe(string $promotionAmount): void + { + $this->responseChecker->hasItemWithValues( + $this->getAdjustmentsResponseForOrder(true), + [ + 'type' => AdjustmentInterface::ORDER_SHIPPING_PROMOTION_ADJUSTMENT, + 'amount' => $this->getTotalAsInt($promotionAmount), + ], + ); + } + + /** + * @Then there should be a shipping charge :shippingCharge for :shippingMethodName method + */ + public function thereShouldBeAShippingChargeForMethod(string $shippingCharge, string $shippingMethodName): void + { + $this->responseChecker->hasItemWithValues( + $this->getAdjustmentsResponseForOrder(true), + [ + 'type' => AdjustmentInterface::SHIPPING_ADJUSTMENT, + 'label' => $shippingMethodName, + 'amount' => $this->getTotalAsInt($shippingCharge), + ], + ); + } + + /** + * @Then there should be a shipping tax :shippingTax for :shippingMethodName method + */ + public function thereShouldBeAShippingTaxForMethod(string $shippingTax, string $shippingMethodName): void + { + $this->responseChecker->hasItemWithValues( + $this->getAdjustmentsResponseForOrder(true), + [ + 'type' => AdjustmentInterface::TAX_ADJUSTMENT, + 'label' => $shippingMethodName, + 'amount' => $this->getTotalAsInt($shippingTax), + ], + ); + } + /** * @Then /^(the administrator) should see that (order placed by "[^"]+") has "([^"]+)" currency$/ */ @@ -311,4 +620,633 @@ final class ManagingOrdersContext implements Context Assert::same($currencyCode, $currency); } + + /** + * @Then I should see an order with :orderNumber number + */ + public function iShouldSeeOrderWithNumber(string $orderNumber): void + { + $response = $this->client->getLastResponse(); + + Assert::true( + $this->responseChecker->hasItemWithValue($response, 'number', $orderNumber), + sprintf('No order with number "%s" has been found.', $orderNumber), + ); + } + + /** + * @Then I should not see an order with :orderNumber number + */ + public function iShouldNotSeeOrderWithNumber(string $orderNumber) + { + $response = $this->client->getLastResponse(); + + Assert::false( + $this->responseChecker->hasItemWithValue($response, 'number', $orderNumber), + sprintf('The order with number "%s" has been found, but should not.', $orderNumber), + ); + } + + /** + * @Then I should not see any orders with currency :currencyCode + */ + public function iShouldNotSeeAnyOrderWithCurrency(string $currencyCode): void + { + $response = $this->client->getLastResponse(); + + Assert::false( + $this->responseChecker->hasItemWithValue($response, 'currencyCode', $currencyCode), + sprintf('The order with currency code "%s" has been found, but should not.', $currencyCode), + ); + } + + /** + * @Then the first order should have number :number + */ + public function theFirstOrderShouldHaveNumber(string $number): void + { + $items = $this->responseChecker->getValue($this->client->getLastResponse(), 'hydra:member'); + $firstItem = $items[0]; + + Assert::same($firstItem['number'], str_replace('#', '', $number)); + } + + /** + * @Then /^I should see the order "([^"]+)" with total ("[^"]+")$/ + */ + public function iShouldSeeTheOrderWithTotal(string $orderNumber, int $total): void + { + $order = $this->responseChecker->getCollectionItemsWithValue( + $this->client->getLastResponse(), + 'number', + trim($orderNumber, '#'), + )[0]; + + Assert::same( + $order['total'], + $total, + ); + } + + /** + * @Then the administrator should see the order with total :total in order list + */ + public function theAdministratorShouldSeeTheOrderWithTotalInOrderList(string $total): void + { + $adminUser = $this->sharedStorage->get('administrator'); + $currencyCode = $this->getCurrencyCodeFromTotal($total); + $total = $this->getTotalAsInt($total); + + $this->sharedSecurityService->performActionAsAdminUser( + $adminUser, + fn () => $this->client->index(Resources::ORDERS), + ); + + $itemsWithCurrency = $this->responseChecker->getCollectionItemsWithValue( + $this->client->getLastResponse(), + 'currencyCode', + $currencyCode, + ); + + $firstItem = array_pop($itemsWithCurrency); + + Assert::notEmpty($firstItem); + Assert::same($firstItem['total'], $total); + } + + /** + * @Then it should have been placed by the customer :customer + */ + public function itShouldHaveBeenPlacedByTheCustomer(CustomerInterface $customer): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'customer'), + $this->iriConverter->getIriFromResource($customer), + ); + } + + /** + * @Then it should be shipped via the :shippingMethod shipping method + */ + public function itShouldBeShippedViaTheShippingMethod(ShippingMethodInterface $shippingMethod): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'shipments')[0]['method'], + $this->iriConverter->getIriFromResource($shippingMethod), + ); + } + + /** + * @Then it should be paid with :paymentMethod + */ + public function itShouldBePaidWith(PaymentMethodInterface $paymentMethod): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'payments')[0]['method'], + $this->iriConverter->getIriFromResource($paymentMethod), + ); + } + + /** + * @Then it should have no shipping address set + */ + public function itShouldHaveNoShippingAddressSet(): void + { + Assert::false($this->responseChecker->hasKey($this->client->getLastResponse(), 'shippingAddress')); + } + + /** + * @Then it should be shipped to :customerName, :street, :postcode, :city, :countryName + */ + public function itShouldBeShippedTo( + string $customerName, + string $street, + string $postcode, + string $city, + string $countryName, + ): void { + $shippingAddress = $this->responseChecker->getValue($this->client->getLastResponse(), 'shippingAddress'); + + $this->itShouldBeAddressedTo( + $shippingAddress, + $customerName, + $street, + $postcode, + $city, + $countryName, + ); + } + + /** + * @Then it should have :customerName, :street, :postcode, :city, :countryName as its billing address + */ + public function itShouldHaveAddressAsItBillingAddress( + string $customerName, + string $street, + string $postcode, + string $city, + string $countryName, + ): void { + $billingAddress = $this->responseChecker->getValue($this->client->getLastResponse(), 'billingAddress'); + + $this->itShouldBeAddressedTo( + $billingAddress, + $customerName, + $street, + $postcode, + $city, + $countryName, + ); + } + + /** + * @Then I should see :provinceName as province in the shipping address + */ + public function iShouldSeeAsProvinceInTheShippingAddress(string $provinceName): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'shippingAddress')['provinceName'], + $provinceName, + ); + } + + /** + * @Then I should see :provinceName as province in the billing address + */ + public function iShouldSeeAsProvinceInTheBillingAddress(string $provinceName): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'billingAddress')['provinceName'], + $provinceName, + ); + } + + /** + * @Then I should see the shipping date as :dateTime + */ + public function iShouldSeeTheShippingDateAs(string $dateTime): void + { + $response = $this->client->show(Resources::SHIPMENTS, (string) $this->sharedStorage->get('shipment')->getId()); + + Assert::same( + $this->responseChecker->getValue($response, 'shippedAt'), + (new \DateTime($dateTime))->format('Y-m-d H:i:s'), + ); + } + + /** + * @Then /^(its) unit price should be ([^"]+)$/ + */ + public function itemUnitPriceShouldBe(array $orderItem, string $unitPrice): void + { + Assert::same($this->getTotalAsInt($unitPrice), $orderItem['unitPrice']); + } + + /** + * @Then /^(its) total should be ([^"]+)$/ + */ + public function itemTotalShouldBe(array $orderItem, string $total): void + { + Assert::same($this->getTotalAsInt($total), $orderItem['total']); + } + + /** + * @Then /^(its) code should be "([^"]+)"$/ + */ + public function itemCodeShouldBe(array $orderItem, string $code): void + { + Assert::endsWith($orderItem['variant'], $code); + } + + /** + * @Then /^(its) quantity should be ([^"]+)$/ + */ + public function itemQuantityShouldBe(array $orderItem, int $quantity): void + { + Assert::same($quantity, $orderItem['quantity']); + } + + /** + * @Then /^its discounted unit price should be ([^"]+)$/ + */ + public function itemDiscountedUnitPriceShouldBe(string $discountedUnitPrice): void + { + $this->responseChecker->hasItemWithValues( + $this->getAdjustmentsResponseForOrder(), + [ + 'type' => AdjustmentInterface::ORDER_ITEM_PROMOTION_ADJUSTMENT, + 'amount' => $this->getTotalAsInt($discountedUnitPrice), + ], + ); + } + + /** + * @Then /^its subtotal should be ([^"]+)$/ + */ + public function itemSubtotalShouldBe(string $subtotal): void + { + $orderItem = $this->sharedStorage->get('item'); + + $unitPromotionAdjustments = 0; + foreach ($this->responseChecker->getCollection($this->client->getLastResponse()) as $item) { + if (in_array($item['type'], [AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT])) { + $unitPromotionAdjustments += $item['amount']; + } + } + + Assert::same($this->getTotalAsInt($subtotal), $orderItem['unitPrice'] * $orderItem['quantity'] + $unitPromotionAdjustments); + } + + /** + * @Then /^its discount should be ([^"]+)$/ + */ + public function theItemShouldHaveDiscount(string $discount): void + { + $this->responseChecker->hasItemWithValues( + $this->client->getLastResponse(), + [ + 'type' => AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, + 'amount' => $this->getTotalAsInt($discount), + ], + ); + } + + /** + * @Then /^its tax should be ([^"]+)$/ + */ + public function itemTaxShouldBe(string $tax): void + { + $this->responseChecker->hasItemWithValues( + $this->client->getLastResponse(), + [ + 'type' => AdjustmentInterface::TAX_ADJUSTMENT, + 'amount' => $this->getTotalAsInt($tax), + ], + ); + } + + /** + * @Then /^its tax included in price should be ([^"]+)$/ + */ + public function itsTaxIncludedInPriceShouldBe(string $tax): void + { + $unitPromotionAdjustments = $this->responseChecker->getCollectionItemsWithValue( + $this->getAdjustmentsResponseForOrder(), + 'type', + AdjustmentInterface::TAX_ADJUSTMENT, + ); + $totalTax = 0; + + foreach ($unitPromotionAdjustments as $unitPromotionAdjustment) { + if (true === $unitPromotionAdjustment['neutral']) { + $totalTax += $unitPromotionAdjustment['amount']; + } + } + + Assert::same($this->getTotalAsInt($tax), $totalTax); + } + + /** + * @Then I should be informed that there are no payments + */ + public function iShouldSeeInformationAboutNoPayments(): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'payments'), + [], + ); + } + + /** + * @Then /^the order "[^"]+" should have order shipping state "([^"]+)"$/ + * @Then it should have order's shipping state :orderShippingState + */ + public function theOrderShouldHaveShippingState(string $orderShippingState): void + { + $ordersResponse = $this->client->index(Resources::ORDERS, forgetResponse: true); + + Assert::true( + $this->responseChecker->hasItemWithValue($ordersResponse, 'shippingState', strtolower($orderShippingState)), + sprintf('Order does not have %s shipping state', $orderShippingState), + ); + } + + /** + * @Then I should not see information about shipments + */ + public function iShouldNotSeeInformationAboutShipping(): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'shipments'), + [], + ); + } + + /** + * @Then the :productName product's unit price should be :price + */ + public function productUnitPriceShouldBe(string $productName, string $price): void + { + $this->iCheckData($productName); + $orderItem = $this->sharedStorage->get('item'); + Assert::same($this->getTotalAsInt($price), $orderItem['unitPrice']); + } + + /** + * @Then the :productName product's discounted unit price should be :price + */ + public function productDiscountedUnitPriceShouldBe(string $productName, string $price): void + { + $orderItem = $this->sharedStorage->get('item'); + Assert::same($this->getTotalAsInt($price), $orderItem['fullDiscountedUnitPrice']); + } + + /** + * @Then the :productName product's quantity should be :quantity + */ + public function productQuantityShouldBe(string $productName, int $quantity): void + { + $orderItem = $this->sharedStorage->get('item'); + Assert::same($quantity, $orderItem['quantity']); + } + + /** + * @Then the :productName product's item discount should be :price + */ + public function productItemDiscountShouldBe(string $productName, string $price): void + { + $orderItem = $this->sharedStorage->get('item'); + + $adjustments = $this->responseChecker->getCollectionItemsWithValue( + $this->getAdjustmentsResponseForOrder(true), + 'type', + AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, + ); + + foreach ($adjustments as $adjustment) { + if (in_array($adjustment['orderItemUnit'], $orderItem['units'])) { + Assert::same($this->getTotalAsInt($price), $adjustment['amount']); + + return; + } + } + } + + /** + * @Then the :productName product's order discount should be :price + */ + public function productOrderDiscountShouldBe(string $productName, string $price): void + { + $orderItem = $this->sharedStorage->get('item'); + + $adjustments = $this->responseChecker->getCollectionItemsWithValue( + $this->getAdjustmentsResponseForOrder(true), + 'type', + AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT, + ); + + foreach ($adjustments as $adjustment) { + if (in_array($adjustment['orderItemUnit'], $orderItem['units'])) { + Assert::same($this->getTotalAsInt(trim($price, ' ~')), $adjustment['amount']); + + return; + } + } + } + + /** + * @Then the :productName product's subtotal should be :subTotal + */ + public function productSubtotalShouldBe(string $productName, string $subTotal): void + { + $orderItem = $this->sharedStorage->get('item'); + $response = $this->getAdjustmentsResponseForOrder(true); + + $unitPromotionAdjustments = 0; + foreach ($this->responseChecker->getCollection($response) as $adjustment) { + if (in_array($adjustment['type'], [AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT, AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT])) { + if (in_array($adjustment['orderItemUnit'], $orderItem['units'])) { + $unitPromotionAdjustments += $adjustment['amount']; + } + } + } + + Assert::same($this->getTotalAsInt($subTotal), $orderItem['unitPrice'] * $orderItem['quantity'] + $unitPromotionAdjustments); + } + + /** + * @Then I should be notified that the order has been successfully shipped + */ + public function iShouldBeNotifiedThatTheOrderHasBeenSuccessfullyShipped(): void + { + $response = $this->client->getLastResponse(); + Assert::true( + $this->responseChecker->isAccepted($response), + 'Order could not be shipped.', + ); + } + + /** + * @Then it should have shipment in state shipped + */ + public function itShouldHaveShipmentInStateShipped(): void + { + $shipmentIri = $this->responseChecker->getValue( + $this->client->show(Resources::ORDERS, $this->sharedStorage->get('order')->getTokenValue()), + 'shipments', + )[0]; + + Assert::true( + $this->responseChecker->hasValue($this->client->showByIri($shipmentIri['@id']), 'state', ShipmentInterface::STATE_SHIPPED), + sprintf('Shipment for this order is not %s', ShipmentInterface::STATE_SHIPPED), + ); + } + + /** + * @Then this order should have order shipping state :orderShippingState + */ + public function thisOrderShouldHaveOrderShippingState(string $orderShippingState): void + { + $ordersResponse = $this->client->index(Resources::ORDERS); + + Assert::true( + $this->responseChecker->hasItemWithValue($ordersResponse, 'shippingState', strtolower($orderShippingState)), + sprintf('Order does not have %s shipping state', $orderShippingState), + ); + } + + /** + * @Then I should not be able to ship this order + */ + public function iShouldNotBeAbleToShipThisOrder(): void + { + $order = $this->sharedStorage->get('order'); + + $this->client->applyTransition( + Resources::SHIPMENTS, + (string) $order->getShipments()->first()->getId(), + ShipmentTransitions::TRANSITION_SHIP, + ); + + Assert::false( + $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), + 'Order has been shipped, but should not.', + ); + } + + /** + * @Then I should be informed that the order does not exist + */ + public function iShouldBeInformedThatTheOrderDoesNotExist(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Not Found', + ); + } + + /** + * @Then there should be :count shipping address changes in the registry + */ + public function thereShouldBeCountShippingAddressChangesInTheRegistry(int $count): void + { + $order = $this->sharedStorage->get('order'); + $response = $this->client->subResourceIndex( + Resources::ADDRESSES, + Subresources::ADDRESSES_LOG_ENTRIES, + (string) $order->getShippingAddress()->getId(), + ); + Assert::same($this->responseChecker->countCollectionItems($response), $count); + } + + /** + * @Then I should not be able to resend the order confirmation email + */ + public function iShouldNotBeAbleToResendTheOrderConfirmationEmail(): void + { + $this->client->customItemAction( + Resources::ORDERS, + $this->sharedStorage->get('order')->getTokenValue(), + HttpRequest::METHOD_POST, + 'resend-confirmation-email', + ); + + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Cannot resend order confirmation email for order with state cancelled.', + ); + } + + /** + * @Then there should be :count billing address changes in the registry + */ + public function thereShouldBeCountBillingAddressChangesInTheRegistry(int $count): void + { + $order = $this->sharedStorage->get('order'); + $response = $this->client->subResourceIndex( + Resources::ADDRESSES, + Subresources::ADDRESSES_LOG_ENTRIES, + (string) $order->getBillingAddress()->getId(), + ); + Assert::same($this->responseChecker->countCollectionItems($response), $count); + } + + /** + * @param array $address + */ + private function itShouldBeAddressedTo( + array $address, + string $customerName, + string $street, + string $postcode, + string $city, + string $countryName, + ): void { + Assert::same($address['firstName'] . ' ' . $address['lastName'], $customerName); + Assert::same($address['street'], $street); + Assert::same($address['postcode'], $postcode); + Assert::same($address['city'], $city); + Assert::same($address['countryCode'], $this->getCountryCodeFromName($countryName)); + } + + private function getCountryCodeFromName(string $name): string + { + return array_flip(Countries::getNames())[$name]; + } + + private function getCurrencyCodeFromTotal(string $total): string + { + return match (true) { + str_starts_with($total, '$') => 'USD', + str_starts_with($total, '€') => 'EUR', + str_starts_with($total, '£') => 'GBP', + default => throw new \InvalidArgumentException('Unsupported currency symbol'), + }; + } + + private function getTotalAsInt(string $total): int + { + if ($isMinus = str_starts_with($total, '-')) { + $total = substr($total, 1); + } + $amount = (int) round((float) trim($total, '$€£') * 100, 2); + + if ($isMinus) { + return $amount * -1; + } + + return $amount; + } + + private function getAdjustmentsResponseForOrder(bool $forgetResponse = false): Response + { + $orderToken = $this->sharedStorage->get('order')->getTokenValue(); + + return $this->client->subResourceIndex( + Resources::ORDERS, + Resources::ADJUSTMENTS, + (string) $orderToken, + forgetResponse: $forgetResponse, + ); + } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentMethodsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentMethodsContext.php new file mode 100644 index 0000000000..f998c1d4d8 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentMethodsContext.php @@ -0,0 +1,780 @@ + 'asc', 'descending' => 'desc']; + + public function __construct( + private ApiClientInterface $client, + private ResponseCheckerInterface $responseChecker, + private SectionAwareIriConverter $sectionAwareIriConverter, + private SharedStorageInterface $sharedStorage, + ) { + } + + /** + * @When I want to modify the :paymentMethod payment method + */ + public function iWantToModifyAPaymentMethod(PaymentMethodInterface $paymentMethod): void + { + $this->client->buildUpdateRequest(Resources::PAYMENT_METHODS, $paymentMethod->getCode()); + } + + /** + * @When I name it :name in :localeCode + * @When I rename it to :name in :localeCode + * @When I remove its name from :localeCode translation + */ + public function iNameItIn(string $localeCode, ?string $name = null): void + { + $this->client->addRequestData('translations', [$localeCode => ['name' => $name]]); + } + + /** + * @When I do not name it + */ + public function iDoNotNameIt(): void + { + // Intentionally left blank to fulfill context expectation + } + + /** + * @When I enable it + */ + public function iEnableIt(): void + { + $this->client->addRequestData('enabled', true); + } + + /** + * @When I disable it + */ + public function iDisableIt(): void + { + $this->client->addRequestData('enabled', false); + } + + /** + * @When I delete the :paymentMethod payment method + * @When I try to delete the :paymentMethod payment method + */ + public function iDeletePaymentMethod(PaymentMethodInterface $paymentMethod): void + { + $this->client->delete(Resources::PAYMENT_METHODS, $paymentMethod->getCode()); + } + + /** + * @When I want to create a new offline payment method + * @When I want to create a new payment method with :factory gateway factory + */ + public function iWantToCreateANewPaymentMethod(string $factory = 'Offline'): void + { + $factory = str_replace(' ', '_', strtolower($factory)); + + $this->client->buildCreateRequest(Resources::PAYMENT_METHODS); + $this->client->addRequestData('gatewayConfig', ['factoryName' => $factory, 'gatewayName' => $factory]); + } + + /** + * @When I want to create a new payment method without gateway configuration + */ + public function iWantToCreateANewPaymentMethodWithoutGatewayConfiguration(): void + { + $this->client->buildCreateRequest(Resources::PAYMENT_METHODS); + $this->client->addRequestData('code', 'TEST'); + } + + /** + * @When I want to create a new payment method without gateway name + */ + public function iWantToCreateANewPaymentMethodWithoutGatewayName(): void + { + $this->client->buildCreateRequest(Resources::PAYMENT_METHODS); + $this->client->addRequestData('code', 'TEST'); + $this->client->addRequestData('gatewayConfig', ['factoryName' => 'offline']); + } + + /** + * @When I want to create a new payment method without factory name + */ + public function iWantToCreateANewPaymentMethodWithoutFactoryName(): void + { + $this->client->buildCreateRequest(Resources::PAYMENT_METHODS); + $this->client->addRequestData('code', 'TEST'); + $this->client->addRequestData('gatewayConfig', ['gatewayName' => 'offline']); + } + + /** + * @When I want to create a new payment method with wrong factory name + */ + public function iWantToCreateANewPaymentMethodWithWrongFactoryName(): void + { + $this->client->buildCreateRequest(Resources::PAYMENT_METHODS); + $this->client->addRequestData('code', 'TEST'); + $this->client->addRequestData('gatewayConfig', ['factoryName' => 'gateway_with_wrong_factory_name', 'gatewayName' => 'gateway with wrong factory name']); + } + + /** + * @When I specify its code as :code + * @When I do not specify its code + */ + public function iSpecifyItsCodeAs(string $code = null): void + { + $this->client->addRequestData('code', $code); + } + + /** + * @When I describe it as :description in :localeCode + */ + public function iDescribeItAsIn(string $description, string $localeCode): void + { + $this->client->addRequestData('translations', [$localeCode => ['description' => $description]]); + } + + /** + * @When make it available in channel :channel + */ + public function iMakeItAvailableInChannel(ChannelInterface $channel): void + { + $this->client->replaceRequestData('channels', [$this->sectionAwareIriConverter->getIriFromResourceInSection($channel, 'admin')]); + } + + /** + * @When I set its instruction as :instructions in :localeCode + */ + public function iSetItsInstructionAsIn(string $instructions, string $localeCode): void + { + $this->client->addRequestData('translations', [$localeCode => ['instructions' => $instructions]]); + } + + /** + * @When I add it + * @When I try to add it + */ + public function iAddIt(): void + { + $this->client->create(); + } + + /** + * @When I cancel my changes + */ + public function iCancelMyChanges(): void + { + $this->createPage->cancelChanges(); + } + + /** + * @When I start sorting payment methods by name + * @When the payment methods are already sorted by name + * @When I switch the way payment methods are sorted to :sortType by name + */ + public function iSortShippingMethodsByName(string $sortType = 'ascending'): void + { + $this->client->sort([ + 'translation.name' => self::SORT_TYPES[$sortType], + 'localeCode' => $this->getAdminLocaleCode(), + ]); + } + + /** + * @When I start sorting payment methods by code + * @When I switch the way payment methods are sorted to :sortType by code + * @Given the payment methods are already sorted by code + */ + public function iSortShippingMethodsByCode(string $sortType = 'ascending'): void + { + $this->client->sort([ + 'code' => self::SORT_TYPES[$sortType], + 'localeCode' => $this->getAdminLocaleCode(), + ]); + } + + /** + * @When I configure it with test paypal credentials + */ + public function iConfigureItWithTestPaypalCredentials(): void + { + $this->client->addRequestData( + 'gatewayConfig', + [ + 'config' => [ + 'username' => 'test', + 'password' => 'test', + 'signature' => 'test', + 'sandbox' => true, + ], + ], + ); + } + + /** + * @When I configure it for username :username with :signature signature + */ + public function iConfigureItForUsernameWithSignature(string $username, string $signature): void + { + $this->client->addRequestData( + 'gatewayConfig', + [ + 'config' => [ + 'username' => $username, + 'signature' => $signature, + 'sandbox' => true, + ], + ], + ); + } + + /** + * @When I configure it for username :username with :signature signature and password, but without sandbox + */ + public function iConfigureItForUsernameWithSignatureButWithoutSandbox(string $username, string $signature): void + { + $this->client->addRequestData( + 'gatewayConfig', + [ + 'config' => [ + 'username' => $username, + 'signature' => $signature, + 'password' => 'TEST', + 'sandbox' => null, + ], + ], + ); + } + + /** + * @When I configure it for username :username with :signature signature and password, but with sandbox that has wrong type + */ + public function iConfigureItForUsernameWithSignatureButWithWrongSandboxType(string $username, string $signature): void + { + $this->client->addRequestData( + 'gatewayConfig', + [ + 'config' => [ + 'username' => $username, + 'signature' => $signature, + 'password' => 'TEST', + 'sandbox' => 'test', + ], + ], + ); + } + + /** + * @When I configure it with only :element + */ + public function iConfigureItWithOnly(string $element): void + { + $element = str_replace(' ', '_', strtolower($element)); + + $this->client->addRequestData( + 'gatewayConfig', + [ + 'config' => [ + $element => 'TEST', + $element === 'secret_key' ? 'publishable_key' : 'secret_key' => null, + ], + ], + ); + } + + /** + * @When I do not specify configuration password + */ + public function iDoNotSpecifyConfigurationPassword(): void + { + $this->client->addRequestData( + 'gatewayConfig', + [ + 'config' => [ + 'password' => null, + ], + ], + ); + } + + /** + * @When I configure it with test stripe gateway data + */ + public function iConfigureItWithTestStripeGatewayData(): void + { + $this->client->addRequestData( + 'gatewayConfig', + [ + 'config' => [ + 'publishable_key' => 'test', + 'secret_key' => 'test', + ], + ], + ); + } + + /** + * @Given I am browsing payment methods + * @When I browse payment methods + */ + public function iBrowsePaymentMethods(): void + { + $this->client->index(Resources::PAYMENT_METHODS); + } + + /** + * @When I change my locale to :localeCode + */ + public function iChangeMyLocaleTo(string $localeCode): void + { + /** @var AdminUserInterface $adminUser */ + $adminUser = $this->sharedStorage->get('administrator'); + + $this->client->buildUpdateRequest(Resources::ADMINISTRATORS, (string) $adminUser->getId()); + + $this->client->updateRequestData(['localeCode' => $localeCode]); + $this->client->update(); + } + + /** + * @Then the first payment method on the list should have :field :value + */ + public function theFirstPaymentMethodOnTheListShouldHave(string $field, string $value): void + { + $response = $this->client->getLastResponse(); + + $paymentMethods = $this->responseChecker->getCollection($response); + + Assert::same($this->getFieldValueOfFirstPaymentMethod($paymentMethods[0], $field), $value); + } + + /** + * @Then the last payment method on the list should have :field :value + */ + public function theLastPaymentMethodOnTheListShouldHave(string $field, string $value): void + { + $response = $this->client->index(Resources::PAYMENT_METHODS); + + if ($field = 'name') { + $paymentMethods = $this->responseChecker->getCollection($response); + + Assert::same(end($paymentMethods)['translations']['en_US']['name'], $value); + + return; + } + + $count = $this->responseChecker->countCollectionItems($response); + + Assert::true( + $this->responseChecker->hasItemOnPositionWithValue($this->client->getLastResponse(), $count - 1, $field, $value), + sprintf('There should be payment method with %s "%s" on position %d, but it does not.', $field, $value, $count - 1), + ); + } + + /** + * @Then I should see a single payment method in the list + * @Then I should see :amount payment methods in the list + */ + public function iShouldSeePaymentMethodsInTheList(int $amount = 1): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $amount); + } + + /** + * @Then I should be notified that :element is required + */ + public function iShouldBeNotifiedThatElementIsRequired(string $element): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('The type of the "%s" attribute must be "string", "NULL" given.', $element), + ); + } + + /** + * @Then I should be notified that I have to specify payment method :element + */ + public function iShouldBeNotifiedThatINeedToSpecifyPaymentMethodName(string $element): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('%s: Please enter payment method %s.', $element, $element), + ); + } + + /** + * @Then I should be notified that I have to specify paypal :element + */ + public function iShouldBeNotifiedThatIHaveToSpecifyPaypal(string $element): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('gatewayConfig.config[%s]: Please enter paypal %s.', $element, $element), + ); + } + + /** + * @Then I should be notified that I have to specify paypal sandbox status + */ + public function iShouldBeNotifiedThatIHaveToSpecifyPaypalSandboxStatus(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'gatewayConfig.config[sandbox]: Please set your paypal sandbox status.', + ); + } + + /** + * @Then I should be notified that I have to specify paypal sandbox status that is boolean + */ + public function iShouldBeNotifiedThatIHaveToSpecifyPaypalSandboxStatusThatIsBoolean(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'gatewayConfig.config[sandbox]: This value should be of type bool.', + ); + } + + /** + * @Then I should be notified that I have to specify stripe :element + */ + public function iShouldBeNotifiedThatIHaveToSpecifyStripe(string $element): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('gatewayConfig.config[%s]: Please enter stripe %s.', str_replace(' ', '_', strtolower($element)), $element), + ); + } + + /** + * @Then I should be notified that I have to specify gateway configuration + */ + public function iShouldBeNotifiedThatIHaveToSpecifyGatewayConfiguration(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'gatewayConfig: This value should not be blank.', + ); + } + + /** + * @Then I should be notified that I have to specify gateway name + */ + public function iShouldBeNotifiedThatIHaveToSpecifyGatewayName(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'gatewayConfig.gatewayName: Please enter gateway name.', + ); + } + + /** + * @Then I should be notified that I have to specify factory name + */ + public function iShouldBeNotifiedThatIHaveToSpecifyFactoryName(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'gatewayConfig.factoryName: Please enter gateway factory name.', + ); + } + + /** + * @Then I should be notified that I have to specify factory name that is available + */ + public function iShouldBeNotifiedThatIHaveToSpecifyFactoryNameThatIsAvailable(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'gatewayConfig.factoryName: Invalid gateway factory. Available factories are paypal_express_checkout, stripe_checkout, offline.', + ); + } + + /** + * @Then the payment method with :element :value should not be added + */ + public function thePaymentMethodWithElementValueShouldNotBeAdded(string $element, string $value): void + { + if ($element === 'name') { + Assert::false( + in_array( + $value, + $this->getPaymentMethodNamesFromCollection(), + ), + sprintf('Payment method should have name "%s", but it does not', $value), + ); + + return; + } + + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::PAYMENT_METHODS), $element, $value), + sprintf('Payment method with %s: %s exists', $element, $value), + ); + } + + /** + * @Then this payment method should still be named :paymentMethodName + */ + public function thisPaymentMethodNameShouldStillBeNamed(string $paymentMethodName): void + { + Assert::inArray( + $paymentMethodName, + $this->getPaymentMethodNamesFromCollection(), + sprintf('Payment method with name %s does not exist', $paymentMethodName), + ); + } + + /** + * @Then the code field should be disabled + * @Then I should not be able to edit its code + */ + public function theCodeFieldShouldBeDisabled(): void + { + $this->client->updateRequestData(['code' => 'NEW_CODE']); + + Assert::false($this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE')); + } + + /** + * @Then the factory name field should be disabled + */ + public function theFactoryNameFieldShouldBeDisabled(): void + { + $paymentMethodCode = $this->responseChecker->getValue($this->client->getLastResponse(), 'code'); + + $this->client->addRequestData('gatewayConfig', ['factoryName' => 'NEWFACTORYNAME']); + $this->client->update(); + + Assert::false($this->responseChecker->hasValue($this->client->customItemAction(Resources::PAYMENT_METHODS, $paymentMethodCode, HttpRequest::METHOD_GET, 'gateway-config'), 'factoryName', 'NEWFACTORYNAME')); + } + + /** + * @Then /^(this payment method) should be enabled/ + */ + public function thisPaymentMethodShouldBeEnabled(PaymentMethodInterface $paymentMethod): void + { + Assert::true( + $this->responseChecker->hasValue( + $this->client->show(Resources::PAYMENT_METHODS, $paymentMethod->getCode()), + 'enabled', + true, + ), + 'This payment method should be enabled', + ); + } + + /** + * @Then /^(this payment method) should be disabled$/ + */ + public function thisShippingMethodShouldBeDisabled(PaymentMethodInterface $paymentMethod): void + { + Assert::true( + $this->responseChecker->hasValue( + $this->client->show(Resources::PAYMENT_METHODS, $paymentMethod->getCode()), + 'enabled', + false, + ), + 'This payment method should be disabled', + ); + } + + /** + * @Then the payment method :paymentMethod should have instructions :instructions in :localeCode + */ + public function thePaymentMethodShouldHaveInstructionsIn( + PaymentMethodInterface $paymentMethod, + string $instructions, + string $localeCode, + ): void { + $translations = $this->responseChecker->getValue($this->client->show(Resources::PAYMENT_METHODS, $paymentMethod->getCode()), 'translations'); + + Assert::same( + $translations[$localeCode]['instructions'], + $instructions, + sprintf('Payment method does not have %s instruction', $instructions), + ); + } + + /** + * @Then the payment method :paymentMethod should be available in channel :channel + */ + public function thePaymentMethodShouldBeAvailableInChannel( + PaymentMethodInterface $paymentMethod, + ChannelInterface $channel, + ): void { + $this->client->show(Resources::PAYMENT_METHODS, $paymentMethod->getCode()); + $channelsArray = $this->responseChecker->getValue($this->client->getLastResponse(), 'channels'); + + Assert::true(in_array($this->sectionAwareIriConverter->getIriFromResourceInSection($channel, 'admin'), $channelsArray)); + } + + /** + * @Then /^(this payment method) should no longer exist in the registry$/ + */ + public function thisPaymentMethodShouldNoLongerExistInTheRegistry(PaymentMethodInterface $paymentMethod): void + { + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::PAYMENT_METHODS), 'code', $paymentMethod->getCode()), + sprintf('Payment method with code %s exists but should not', $paymentMethod->getCode()), + ); + } + + /** + * @Then I should be notified that payment method with this code already exists + */ + public function iShouldBeNotifiedThatPaymentMethodWithThisCodeAlreadyExists(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Payment method has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'code: The payment method with given code already exists.', + ); + } + + /** + * @Then there should still be only one payment method with :element :code + */ + public function thereShouldStillBeOnlyOnePaymentMethodWith(string $element, string $code): void + { + $response = $this->client->index(Resources::PAYMENT_METHODS); + $itemsCount = $this->responseChecker->countCollectionItems($response); + + Assert::same($itemsCount, 1, sprintf('Expected 1 payment method, but got %d', $itemsCount)); + Assert::true($this->responseChecker->hasItemWithValue($response, $element, $code)); + } + + /** + * @Then this payment method :element should be :value + */ + public function thisPaymentMethodElementShouldBe( + string $element, + string $value, + ): void { + if ($element === 'name') { + Assert::inArray( + $value, + $this->getPaymentMethodNamesFromCollection(), + sprintf('Payment method should have name "%s", but it does not', $value), + ); + + return; + } + + Assert::true( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::PAYMENT_METHODS), $element, $value), + sprintf('Payment method should have %s "%s", but it does,', $element, $value), + ); + } + + /** + * @Then I should be notified that it has been successfully created + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyCreated(): void + { + Assert::true( + $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), + 'Payment method could not be created', + ); + } + + /** + * @Then I should be notified that it has been successfully deleted + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyDeleted(): void + { + Assert::true( + $this->responseChecker->isDeletionSuccessful($this->client->getLastResponse()), + 'Payment method could not be deleted', + ); + } + + /** + * @Then I should be notified that it is in use + */ + public function iShouldBeNotifiedThatItIsInUse(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Cannot remove, the payment method is in use.', + ); + } + + /** + * @Then the payment method :paymentMethodName should appear in the registry + * @Then the payment method :paymentMethodName should be in the registry + * @Then I should see the payment method :paymentMethodName in the list + */ + public function thePaymentMethodShouldAppearInTheRegistry(string $paymentMethodName): void + { + Assert::inArray( + $paymentMethodName, + $this->getPaymentMethodNamesFromCollection(), + sprintf('Payment method with name %s does not exist', $paymentMethodName), + ); + } + + /** + * @Then /^(this payment method) should still be in the registry$/ + */ + public function thisPaymentMethodShouldStillBeInTheRegistry(PaymentMethodInterface $paymentMethod): void + { + $this->thePaymentMethodShouldAppearInTheRegistry($paymentMethod->getName()); + } + + private function getAdminLocaleCode(): string + { + /** @var AdminUserInterface $adminUser */ + $adminUser = $this->sharedStorage->get('administrator'); + + $response = $this->client->show(Resources::ADMINISTRATORS, (string) $adminUser->getId()); + + return $this->responseChecker->getValue($response, 'localeCode'); + } + + private function getFieldValueOfFirstPaymentMethod(array $paymentMethod, string $field): ?string + { + if ($field === 'code') { + return $paymentMethod['code']; + } + + if ($field === 'name') { + return $paymentMethod['translations'][$this->getAdminLocaleCode()]['name']; + } + + return null; + } + + private function getPaymentMethodNamesFromCollection(): array + { + $paymentMethods = $this->responseChecker->getCollection($this->client->index(Resources::PAYMENT_METHODS)); + + return array_map(fn (array $paymentMethod) => $paymentMethod['translations']['en_US']['name'], $paymentMethods); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentsContext.php index a7b56f6fc5..de65e2fc29 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPaymentsContext.php @@ -13,16 +13,18 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Payment\PaymentTransitions; +use Symfony\Component\HttpFoundation\Request as HttpRequest; use Webmozart\Assert\Assert; final class ManagingPaymentsContext implements Context @@ -30,6 +32,7 @@ final class ManagingPaymentsContext implements Context public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, private IriConverterInterface $iriConverter, private string $apiUrlPrefix, ) { @@ -44,6 +47,34 @@ final class ManagingPaymentsContext implements Context $this->client->index(Resources::PAYMENTS); } + /** + * @When I go to the details of the first payment's order + */ + public function iGoToTheDetailsOfTheFirstPaymentSOrder(): void + { + $firstPayment = $this->responseChecker->getCollection($this->client->getLastResponse())[0]; + + /** @var OrderInterface $order */ + $order = $this->iriConverter->getResourceFromIri($firstPayment['order']); + + $this->client->customItemAction(Resources::ORDERS, $order->getTokenValue(), HttpRequest::METHOD_GET, 'payments'); + } + + /** + * @Then I should see the details of order :order + */ + public function iShouldSeeOrderWithDetails(OrderInterface $order): void + { + Assert::true( + $this->responseChecker->hasItemWithValue( + $this->client->getLastResponse(), + 'order', + $this->sectionAwareIriConverter->getIriFromResourceInSection($order, 'admin'), + ), + sprintf('Order with number %s does not exist', $order->getNumber()), + ); + } + /** * @When I complete the payment of order :order */ @@ -169,7 +200,7 @@ final class ManagingPaymentsContext implements Context Assert::true($this->responseChecker->hasItemWithValue( $this->client->getLastResponse(), 'order', - $this->iriConverter->getIriFromItemInSection($order, 'admin'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($order, 'admin'), )); } @@ -181,7 +212,7 @@ final class ManagingPaymentsContext implements Context Assert::false($this->responseChecker->hasItemWithValue( $this->client->getLastResponse(), 'order', - $this->iriConverter->getIriFromItemInSection($order, 'admin'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($order, 'admin'), )); } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPlacedOrderAddressesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPlacedOrderAddressesContext.php new file mode 100644 index 0000000000..e34b82ef04 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPlacedOrderAddressesContext.php @@ -0,0 +1,169 @@ + */ + private array $addressProperties = [ + 'firstName' => 'getFirstName', + 'lastName' => 'getLastName', + 'street' => 'getStreet', + 'postcode' => 'getPostcode', + 'city' => 'getCity', + 'countryCode' => 'getCountryCode', + ]; + + public function __construct( + private ApiClientInterface $client, + private ResponseCheckerInterface $responseChecker, + private SharedStorageInterface $sharedStorage, + ) { + } + + /** + * @When I want to modify a customer's billing address of this order + */ + public function iWantToModifyCustomerBillingAddress(): void + { + $this->client->buildUpdateRequest( + Resources::ADDRESSES, + (string) $this->sharedStorage->get('order')->getBillingAddress()->getId(), + ); + } + + /** + * @When I want to modify a customer's shipping address of this order + */ + public function iWantToModifyCustomerShippingAddress(): void + { + $this->client->buildUpdateRequest( + Resources::ADDRESSES, + (string) $this->sharedStorage->get('order')->getShippingAddress()->getId(), + ); + } + + /** + * @When /^I clear the (?:billing|shipping) address information$/ + */ + public function iClearTheAddressInformation(): void + { + $this->client->updateRequestData(array_fill_keys(array_keys($this->addressProperties), '')); + } + + /** + * @When /^I do not specify new information$/ + */ + public function iDoNotSpecifyNewInformation(): void + { + // Intentionally left blank to fulfill context expectation + } + + /** + * @When /^I specify their (?:|new )(?:billing|shipping) (address as "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)" for "([^"]+)")$/ + */ + public function iSpecifyTheirAddressAs(AddressInterface $address): void + { + $this->client->addRequestData('firstName', $address->getFirstName()); + $this->client->addRequestData('lastName', $address->getLastName()); + $this->client->addRequestData('street', $address->getStreet()); + $this->client->addRequestData('postcode', $address->getPostcode()); + $this->client->addRequestData('city', $address->getCity()); + } + + /** + * @Then /^this order should(?:| still) have ("([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)" as its(?:| new) billing address)$/ + */ + public function itsBillingAddressShouldContain(AddressInterface $address): void + { + $response = $this->client->show( + Resources::ADDRESSES, + (string) $this->sharedStorage->get('order')->getBillingAddress()->getId(), + ); + + $this->assertAddressResponseProperties($response, $address); + } + + /** + * @Then /^this order should(?:| still) (be shipped to "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)")$/ + */ + public function itShouldBeShippedTo(AddressInterface $address): void + { + $response = $this->client->show( + Resources::ADDRESSES, + (string) $this->sharedStorage->get('order')->getShippingAddress()->getId(), + ); + + $this->assertAddressResponseProperties($response, $address); + } + + /** + * @Then /^I should be notified that all mandatory (?:shipping|billing) address details are incomplete$/ + */ + public function iShouldBeNotifiedThatAllMandatoryAddressDetailsAreIncomplete(): void + { + /** @var array> $mandatoryAddressProperties */ + $mandatoryAddressProperties = [ + 'firstName' => ['minLength' => '2 characters'], + 'lastName' => ['minLength' => '2 characters'], + 'street' => ['minLength' => '2 characters'], + 'city' => ['minLength' => '2 characters'], + 'postcode' => ['minLength' => '1 character'], + ]; + + $violations = $this->responseChecker->getError($this->client->getLastResponse()); + + foreach ($mandatoryAddressProperties as $property => $constraints) { + Assert::contains( + $violations, + sprintf('%s: Please enter %s.', $property, $this->camelCaseToSpaces($property)), + 'Not found violation for ' . $property . ' property.', + ); + + $formattedProperty = ucfirst($this->camelCaseToSpaces($property)); + Assert::contains( + $violations, + sprintf('%s: %s must be at least %s long.', $property, $formattedProperty, $constraints['minLength']), + 'Not found violation for ' . $property . ' property.', + ); + } + + Assert::contains( + $violations, + 'countryCode: Please select country.', + 'Not found violation for countryCode property.', + ); + } + + private function assertAddressResponseProperties(Response $response, AddressInterface $exceptedAddress): void + { + foreach ($this->addressProperties as $property => $getter) { + Assert::same($this->responseChecker->getValue($response, $property), $exceptedAddress->$getter()); + } + } + + private function camelCaseToSpaces(string $string): string + { + return strtolower(preg_replace('/(? [ $localeCode => [ 'name' => $productAssociationTypeName, - 'locale' => $localeCode, ], ], ]); @@ -131,7 +130,7 @@ final class ManagingProductAssociationTypesContext implements Context } /** - * @Then /^I should be notified that (?:it has|they have) been successfully deleted$/ + * @Then /^I should be notified that it has been successfully deleted$/ */ public function iShouldBeNotifiedThatItHasBeenSuccessfullyDeleted(): void { @@ -163,30 +162,11 @@ final class ManagingProductAssociationTypesContext implements Context } /** - * @When I rename it to :name in :language + * @When I rename it to :name in :localeCode */ - public function iRenameItToIn(string $name, string $language): void + public function iRenameItToIn(string $name, string $localeCode): void { - $this->client->updateRequestData(['translations' => [$language => ['name' => $name, 'locale' => $language]]]); - } - - /** - * @When I (try to) save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Product association type could not be edited', - ); + $this->client->updateRequestData(['translations' => [$localeCode => ['name' => $name]]]); } /** @@ -213,29 +193,6 @@ final class ManagingProductAssociationTypesContext implements Context ); } - /** - * @When I check (also) the :productAssociationType product association type - */ - public function iCheckTheProductAssociationType(ProductAssociationTypeInterface $productAssociationType): void - { - $productAssociationTypeToDelete = []; - if ($this->sharedStorage->has('product_association_type_to_delete')) { - $productAssociationTypeToDelete = $this->sharedStorage->get('product_association_type_to_delete'); - } - $productAssociationTypeToDelete[] = $productAssociationType->getCode(); - $this->sharedStorage->set('product_association_type_to_delete', $productAssociationTypeToDelete); - } - - /** - * @When I delete them - */ - public function iDeleteThem(): void - { - foreach ($this->sharedStorage->get('product_association_type_to_delete') as $code) { - $this->client->delete(Resources::PRODUCT_ASSOCIATION_TYPES, $code); - } - } - /** * @When I filter product association types with code containing :value */ @@ -324,7 +281,6 @@ final class ManagingProductAssociationTypesContext implements Context 'translations' => [ $localeCode => [ 'name' => null, - 'locale' => $localeCode, ], ], ]); diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php new file mode 100644 index 0000000000..ffc10e5809 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductAssociationsContext.php @@ -0,0 +1,198 @@ +iAssociateAsTypeTheProductWithTheProducts($type, $owner, [$product]); + } + + /** + * @When /^I (associate as "[^"]+") the (product "[^"]+") with the (products "[^"]+" and "[^"]+")$/ + */ + public function iAssociateAsTypeTheProductWithTheProducts( + ProductAssociationTypeInterface $type, + ProductInterface $owner, + array $products, + ): void { + $associatedProductsData = []; + /** @var ProductInterface $product */ + foreach ($products as $product) { + $associatedProductsData[] = $this->iriConverter->getIriFromItem($product); + } + + $this->client->buildCreateRequest(Resources::PRODUCT_ASSOCIATIONS); + $this->client->addRequestData('type', $this->iriConverter->getIriFromItem($type)); + $this->client->addRequestData('owner', $this->iriConverter->getIriFromItem($owner)); + $this->client->addRequestData('associatedProducts', $associatedProductsData); + $this->client->create(); + + /** @var ProductAssociationInterface $association */ + $association = $this->associationRepository->findOneBy([ + 'owner' => $owner, + 'type' => $type, + ]); + + $this->sharedStorage->set('association', $association); + $this->sharedStorage->set('product', $association->getOwner()); + } + + /** + * @When /^I add the (product "[^"]+") to (this product association)$/ + */ + public function iAddTheProductToThisProductAssociation( + ProductInterface $product, + ProductAssociationInterface $association, + ): void { + $this->client->buildUpdateRequest(Resources::PRODUCT_ASSOCIATIONS, (string) $association->getId()); + + $associatedProducts = [$this->iriConverter->getIriFromItem($product)]; + foreach ($association->getAssociatedProducts() as $associatedProduct) { + $associatedProducts[] = $this->iriConverter->getIriFromItem($associatedProduct); + } + + $this->client->setRequestData(['associatedProducts' => $associatedProducts]); + $this->client->update(); + + $this->sharedStorage->set('association', $association); + } + + /** + * @When /^I change (this product association)'s product to the ("[^"]+" product)$/ + */ + public function iChangeThisProductAssociationProductToProduct( + ProductAssociationInterface $association, + ProductInterface $product, + ): void { + $this->client->buildUpdateRequest(Resources::PRODUCT_ASSOCIATIONS, (string) $association->getId()); + $this->client->addRequestData('associatedProducts', [$this->iriConverter->getIriFromItem($product)]); + $this->client->update(); + + $this->sharedStorage->set('association', $association); + } + + /** + * @When /^I remove the (product "[^"]+") from (this product association)$/ + */ + public function iRemoveTheProductFromThisProductAssociation( + ProductInterface $product, + ProductAssociationInterface $association, + ): void { + $this->client->buildUpdateRequest(Resources::PRODUCT_ASSOCIATIONS, (string) $association->getId()); + + $associatedProducts = []; + foreach ($association->getAssociatedProducts() as $associatedProduct) { + if ($associatedProduct->getCode() !== $product->getCode()) { + $associatedProducts[] = $this->iriConverter->getIriFromItem($associatedProduct); + } + } + + $this->client->setRequestData(['associatedProducts' => $associatedProducts]); + $this->client->update(); + + $this->sharedStorage->set('association', $association); + } + + /** + * @Then /^(this product) should have an (association "[^"]+")$/ + */ + public function thisProductShouldHaveAnAssociation( + ProductInterface $product, + ProductAssociationTypeInterface $type, + ): void { + $response = $this->client->show(Resources::PRODUCTS, $product->getCode()); + $associations = $this->responseChecker->getValue($response, 'associations'); + + $associationTypeIri = $this->sectionAwareIriConverter->getIriFromResourceInSection($type, 'admin'); + + foreach ($associations as $associationIri) { + $response = $this->client->showByIri($associationIri); + $productAssociationType = $this->responseChecker->getValue($response, 'type'); + + if ($associationTypeIri === $productAssociationType) { + return; + } + } + + throw new \InvalidArgumentException(sprintf( + 'Product %s does not have an association of type %s', + $product->getCode(), + $type->getName(), + )); + } + + /** + * @Then /^(this association) should only have (product "[^"]+")$/ + */ + public function thisAssociationShouldOnlyHaveProduct( + ProductAssociationInterface $association, + ProductInterface $product, + ): void { + $this->thisAssociationShouldHaveProducts($association, [$product]); + } + + /** + * @Then /^(this association) should have (products "[^"]+" and "[^"]+")$/ + */ + public function thisAssociationShouldHaveProducts( + ProductAssociationInterface $association, + array $products, + ): void { + $response = $this->client->show(Resources::PRODUCT_ASSOCIATIONS, (string) $association->getId()); + + $content = $this->responseChecker->getResponseContent($response); + $associatedProducts = $content['associatedProducts']; + + Assert::count($associatedProducts, count($products)); + + /** @var ProductInterface $product */ + foreach ($products as $product) { + Assert::inArray( + $this->sectionAwareIriConverter->getIriFromResourceInSection($product, 'admin'), + $associatedProducts, + ); + } + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductAttributesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductAttributesContext.php new file mode 100644 index 0000000000..a0102fb3de --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductAttributesContext.php @@ -0,0 +1,465 @@ +client->index(Resources::PRODUCT_ATTRIBUTES); + } + + /** + * @When /^I(?:| try to) delete (this product attribute)$/ + */ + public function iDeleteThisProductAttribute(ProductAttributeInterface $attribute): void + { + $this->client->delete(Resources::PRODUCT_ATTRIBUTES, $attribute->getCode()); + } + + /** + * @When I want to create a new :type product attribute + */ + public function iWantToCreateANewTypedProductAttribute(string $type): void + { + $this->client->buildCreateRequest(Resources::PRODUCT_ATTRIBUTES); + $this->client->addRequestData('type', $type); + } + + /** + * @When I specify its code as :code + */ + public function iSpecifyItsCodeAs(string $code): void + { + $this->client->addRequestData('code', $code); + } + + /** + * @When I name it :name in :localeCode + * @When I change its name to :name in :localeCode + * @When I do not name it + * @When I remove its name from :localeCode translation + */ + public function iNameItIn(string $name = '', string $localeCode = 'en_US'): void + { + $this->client->updateRequestData(['translations' => [$localeCode => ['name' => $name]]]); + } + + /** + * @When I (also) add value :value in :localeCode + */ + public function iAddValueIn(string $value, string $localeCode): void + { + $uuid = Uuid::uuid4()->toString(); + + $this->client->addRequestData('configuration', ['choices' => [$uuid => [$localeCode => $value]]]); + } + + /** + * @When I disable its translatability + */ + public function iDisableItsTranslatability(): void + { + $this->client->addRequestData('translatable', false); + } + + /** + * @When I check multiple option + */ + public function iCheckMultipleOption(): void + { + $this->client->addRequestData('configuration', ['multiple' => true]); + } + + /** + * @When I do not check multiple option + * @When I do not specify its code + */ + public function intentionallyBlank(): void + { + // Intentionally left blank + } + + /** + * @When I specify its :limitType entries value as :count + * @When I specify its :limitType length as :count + */ + public function iSpecifyItsLimitTypeEntriesAs(string $limitType, int $count): void + { + $this->client->addRequestData('configuration', [$limitType => $count]); + } + + /** + * @When /^I want to edit (this product attribute)$/ + */ + public function iWantToEditThisProductAttribute(ProductAttributeInterface $productAttribute): void + { + $this->sharedStorage->set('product_attribute', $productAttribute); + + $this->client->buildUpdateRequest(Resources::PRODUCT_ATTRIBUTES, $productAttribute->getCode()); + } + + /** + * @When I add it + * @When I try to add it + */ + public function iAddIt(): void + { + $this->client->create(); + } + + /** + * @When /^I change (its) value "([^"]+)" to "([^"]+)"$/ + */ + public function iChangeItsValueTo( + ProductAttributeInterface $productAttribute, + string $oldValue, + string $newValue, + ): void { + $response = $this->client->show(Resources::PRODUCT_ATTRIBUTES, $productAttribute->getCode()); + $configuration = $this->responseChecker->getValue($response, 'configuration'); + + $choices = $configuration['choices']; + foreach ($choices as $key => $choice) { + if ($choice['en_US'] === $oldValue) { + $choices[$key]['en_US'] = $newValue; + + break; + } + } + + $this->client->updateRequestData(['configuration' => ['choices' => $choices]]); + } + + /** + * @When I delete value :value + */ + public function iDeleteValue(string $value): void + { + /** @var ProductAttributeInterface $productAttribute */ + $productAttribute = $this->sharedStorage->get('product_attribute'); + $response = $this->client->show(Resources::PRODUCT_ATTRIBUTES, $productAttribute->getCode()); + $configuration = $this->responseChecker->getValue($response, 'configuration'); + + $choices = $configuration['choices']; + foreach ($choices as $key => $choice) { + if ($choice['en_US'] === $value) { + unset($choices[$key]); + + break; + } + } + + $this->client->setRequestData(['configuration' => ['choices' => $choices]]); + } + + /** + * @Then I should see :count product attributes in the list + */ + public function iShouldSeeCountProductAttributesInTheList(int $count): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $count); + } + + /** + * @Then the first product attribute on the list should have name :name + */ + public function theFirstProductAttributeOnTheListShouldHaveName(string $name): void + { + $first = $this->responseChecker->getCollection($this->client->getLastResponse())[0]; + + Assert::same($first['translations']['en_US']['name'], $name); + } + + /** + * @Then the last product attribute on the list should have name :name + */ + public function theLastProductAttributeOnTheListShouldHaveName(string $name): void + { + $collection = $this->responseChecker->getCollection($this->client->getLastResponse()); + $last = end($collection); + + Assert::same($last['translations']['en_US']['name'], $name); + } + + /** + * @Then I should see the product attribute :attributeName in the list + */ + public function iShouldSeeTheProductAttributeInTheList(string $attributeName): void + { + Assert::true($this->responseChecker->hasItemWithTranslation( + $this->client->getLastResponse(), + 'en_US', + 'name', + $attributeName, + )); + } + + /** + * @Then I should be notified that it has been successfully deleted + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyDeleted(): void + { + $this->responseChecker->isDeletionSuccessful($this->client->getLastResponse()); + } + + /** + * @Then /^(this product attribute) should no longer exist in the registry$/ + */ + public function thisProductAttributeShouldNoLongerExistInTheRegistry(ProductAttributeInterface $productAttribute): void + { + $response = $this->client->index(Resources::PRODUCT_ATTRIBUTES); + + Assert::false( + $this->responseChecker->hasItemWithValue($response, 'code', $productAttribute->getCode()), + sprintf('Product attribute with code %s exists, but should not', $productAttribute->getCode()), + ); + } + + /** + * @Then I should be notified that it is in use + */ + public function iShouldBeNotifiedThatItIsInUse(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Cannot delete, the product attribute is in use.', + ); + } + + /** + * @Then I should be notified that it has been successfully created + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyCreated(): void + { + Assert::true( + $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), + 'Product attribute could not be created', + ); + } + + /** + * @Then the :type attribute :name should appear in the store + * @Then the :type attribute :name should still be in the store + */ + public function theAttributeShouldAppearInTheStore(string $type, string $name): void + { + $response = $this->client->index(Resources::PRODUCT_ATTRIBUTES); + + /** @var array $item */ + foreach ($this->responseChecker->getCollection($response) as $item) { + if ($item['type'] === $type && $item['translations']['en_US']['name'] === $name) { + return; + } + } + + throw new \InvalidArgumentException(sprintf( + 'Product attribute of type "%s" with name "%s" has not been found', + $type, + $name, + )); + } + + /** + * @Then the attribute with :field :value should not appear in the store + */ + public function theAttributeWithCodeShouldNotAppearInTheStore(string $field, string $value): void + { + $response = $this->client->index(Resources::PRODUCT_ATTRIBUTES); + + Assert::false( + $this->responseChecker->hasItemWithValue($response, $field, $value), + sprintf('Product attribute with %s %s exists, but should not', $field, $value), + ); + } + + /** + * @Then I should see the value :value + */ + public function iShouldSeeTheValue(string $value): void + { + $content = $this->responseChecker->getResponseContent($this->client->getLastResponse()); + $choices = $content['configuration']['choices']; + + foreach ($choices as $values) { + if (in_array($value, $values)) { + return; + } + } + + throw new \InvalidArgumentException(sprintf( + 'Product attribute value "%s" has not been found in choices: %s', + $value, + json_encode($choices), + )); + } + + /** + * @Then /^(this product attribute) should have value "([^"]+)"$/ + */ + public function thisProductAttributeShouldHaveValue( + ProductAttributeInterface $productAttribute, + string $value, + ): void { + $this->client->show(Resources::PRODUCT_ATTRIBUTES, $productAttribute->getCode()); + + $this->iShouldSeeTheValue($value); + } + + /** + * @Then /^(this product attribute) should not have value "([^"]+)"$/ + */ + public function thisProductAttributeShouldNotHaveValue( + ProductAttributeInterface $productAttribute, + string $value, + ): void { + $response = $this->client->show(Resources::PRODUCT_ATTRIBUTES, $productAttribute->getCode()); + $content = $this->responseChecker->getResponseContent($response); + $choices = $content['configuration']['choices']; + + foreach ($choices as $values) { + if (in_array($value, $values)) { + throw new \InvalidArgumentException(sprintf( + 'Product attribute value "%s" has been found but should not', + $value, + )); + } + } + } + + /** + * @Then I should be notified that :element is required + */ + public function iShouldBeNotifiedThatFieldIsRequired(string $field): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('Please enter attribute %s.', $field), + ); + } + + /** + * @Then I should be notified that product attribute with this code already exists + */ + public function iShouldBeNotifiedThatProductAttributeWithThisCodeAlreadyExists(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'This code is already in use.', + ); + } + + /** + * @Then I should be notified that max length must be greater or equal to the min length + */ + public function iShouldBeNotifiedThatMaxLengthMustBeGreaterOrEqualToTheMinLength(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Configuration max length must be greater or equal to the min length.', + ); + } + + /** + * @Then there should still be only one product attribute with code :code + */ + public function thereShouldStillBeOnlyOneProductAttributeWithCode(string $code): void + { + $items = $this->responseChecker->getCollectionItemsWithValue( + $this->client->index(Resources::PRODUCT_ATTRIBUTES), + 'code', + $code, + ); + + Assert::count($items, 1, sprintf('More than one attribute with code %s found', $code)); + } + + /** + * @Then I should be notified that max entries value must be greater or equal to the min entries value + */ + public function iShouldBeNotifiedThatMaxEntriesValueMustBeGreaterOrEqualToTheMinEntriesValue(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Configuration max entries value must be greater or equal to the min entries value.', + ); + } + + /** + * @Then I should be notified that min entries value must be lower or equal to the number of added choices + */ + public function iShouldBeNotifiedThatMinEntriesValueMustBeLowerOrEqualToTheNumberOfAddedChoices(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Configuration min entries value must be lower or equal to the number of added choices.', + ); + } + + /** + * @Then I should be notified that multiple must be true if min or max entries values are specified + */ + public function iShouldBeNotifiedThatMultipleMustBeTrueIfMinOrMaxEntriesValuesAreSpecified(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Configuration multiple must be true if min or max entries values are specified.', + ); + } + + /** + * @Then I should not be able to edit its code + */ + public function iShouldNotBeAbleToEditItsCode(): void + { + $this->client->updateRequestData(['code' => 'NEW_CODE']); + + Assert::false( + $this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE'), + 'The code field with value NEW_CODE exist', + ); + } + + /** + * @Then I should not be able to edit its type + */ + public function iShouldNotBeAbleToEditItsType(): void + { + $this->client->updateRequestData(['type' => 'percent']); + + Assert::false( + $this->responseChecker->hasValue($this->client->update(), 'type', 'percent'), + 'The product attribute has new type select set.', + ); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php new file mode 100644 index 0000000000..31e10a4d10 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductImagesContext.php @@ -0,0 +1,252 @@ +createProductImage($path, $product, $type); + } + + /** + * @When /^I attach the "([^"]+)" image to (this product)$/ + */ + public function iAttachTheImageToThisProduct(string $path, ProductInterface $product): void + { + $this->createProductImage($path, $product); + } + + /** + * @When /^I attach the "([^"]+)" image with selected ("[^"]+" variant) to (this product)$/ + */ + public function iAttachImageWithSelectedVariantToThisProduct( + string $path, + ProductVariantInterface $productVariant, + ProductInterface $product, + ): void { + $this->createProductImage($path, $product, null, [$productVariant]); + } + + /** + * @When I( also) remove an image with :type type + */ + public function iRemoveAnImageWithType(string $type): void + { + /** @var ProductInterface $product */ + $product = $this->sharedStorage->get('product'); + + $productImage = $product->getImagesByType($type)->first(); + Assert::notFalse($productImage); + + $this->client->delete(Resources::PRODUCT_IMAGES, (string) $productImage->getId()); + } + + /** + * @When I remove the first image + */ + public function iRemoveTheFirstImage(): void + { + /** @var ProductInterface $product */ + $product = $this->sharedStorage->get('product'); + + $productImage = $product->getImages()->first(); + Assert::notFalse($productImage); + + $this->client->delete(Resources::PRODUCT_IMAGES, (string) $productImage->getId()); + } + + /** + * @When I change the first image type to :type + */ + public function iChangeTheFirstImageTypeTo(string $type): void + { + /** @var ProductInterface $product */ + $product = $this->sharedStorage->get('product'); + + $productImage = $product->getImages()->first(); + Assert::notFalse($productImage); + + $this->client->buildUpdateRequest(Resources::PRODUCT_IMAGES, (string) $productImage->getId()); + $this->client->updateRequestData(['type' => $type]); + $this->client->update(); + } + + /** + * @When I select :productVariant variant for the first image + */ + public function iSelectVariantForTheFirstImage(ProductVariantInterface $productVariant): void + { + /** @var ProductInterface $product */ + $product = $this->sharedStorage->get('product'); + + $productImage = $product->getImages()->first(); + Assert::notFalse($productImage); + + $this->client->buildUpdateRequest(Resources::PRODUCT_IMAGES, (string) $productImage->getId()); + $this->client->updateRequestData(['productVariants' => [$this->sectionAwareIriConverter->getIriFromResourceInSection($productVariant, 'admin')]]); + $this->client->update(); + } + + /** + * @Then the product :product should have an image with :type type + * @Then /^(this product) should(?:| also) have an image with "([^"]*)" type$/ + * @Then /^(it) should(?:| also) have an image with "([^"]*)" type$/ + */ + public function theProductShouldHaveAnImageWithType(ProductInterface $product, string $type): void + { + Assert::true( + $this->responseChecker->hasValuesInAnySubresourceObjectCollection( + $this->client->show(Resources::PRODUCTS, $product->getCode()), + 'images', + ['type' => $type], + ), + sprintf('Product %s does not have an image with %s type', $product->getName(), $type), + ); + } + + /** + * @Then its image should have :productVariant variant selected + */ + public function itsImageShouldHaveVariantSelected(ProductVariantInterface $productVariant): void + { + $images = $this->responseChecker->getValue( + $this->client->getLastResponse(), + 'images', + ); + + Assert::notEmpty($images); + Assert::inArray( + $this->sectionAwareIriConverter->getIriFromResourceInSection($productVariant, 'admin'), + $images[0]['productVariants'], + ); + } + + /** + * @Then /^(this product) should not have(?:| also) any images with "([^"]*)" type$/ + * @Then /^(it) should not have(?:| also) any images with "([^"]*)" type$/ + */ + public function thisProductShouldNotHaveAnyImagesWithType(ProductInterface $product, string $type): void + { + Assert::false( + $this->responseChecker->hasValuesInAnySubresourceObjectCollection( + $this->client->show(Resources::PRODUCTS, $product->getCode()), + 'images', + ['type' => $type], + ), + sprintf('Product %s does not have an image with %s type', $product->getName(), $type), + ); + } + + /** + * @Then /^(this product) should(?:| still) have only one image$/ + * @Then /^(this product) should(?:| still) have (\d+) images?$/ + */ + public function thisProductShouldHaveImages(ProductInterface $product, int $count = 1): void + { + Assert::count( + $this->responseChecker->getValue($this->client->show(Resources::PRODUCTS, $product->getCode()), 'images'), + $count, + ); + } + + /** + * @Then /^(this product) should not have any images$/ + */ + public function thisProductShouldNotHaveAnyImages(ProductInterface $product): void + { + $this->thisProductShouldHaveImages($product, 0); + } + + /** + * @Then I should be notified that the changes have been successfully applied + */ + public function iShouldBeNotifiedThatTheChangesHaveBeenSuccessfullyApplied(): void + { + $response = $this->client->getLastResponse(); + Assert::true( + $this->responseChecker->isDeletionSuccessful($response) || $this->responseChecker->isUpdateSuccessful($response), + ); + } + + /** + * @Then /^I should be notified that the ("[^"]+" variant) does not belong to (this product)$/ + */ + public function iShouldBeNotifiedThatTheProductVariantDoesNotBelongToTheOwner(ProductVariantInterface $productVariant, ProductInterface $product): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf( + 'The product variant with code "%s" does not belong to the product with code "%s", which is the owner of the image.', + $productVariant->getCode(), + $product->getCode(), + ), + ); + } + + private function createProductImage( + string $path, + ProductInterface $product, + ?string $type = null, + array $variants = [], + ): void { + $builder = RequestBuilder::create( + sprintf('/api/v2/admin/products/%s/images', $product->getCode()), + Request::METHOD_POST, + ); + $builder->withHeader('CONTENT_TYPE', 'multipart/form-data'); + $builder->withHeader('HTTP_ACCEPT', 'application/ld+json'); + $builder->withHeader('HTTP_Authorization', 'Bearer ' . $this->sharedStorage->get('token')); + $builder->withFile('file', new UploadedFile($this->minkParameters['files_path'] . $path, basename($path))); + + if (null !== $type) { + $builder->withParameter('type', $type); + } + + if (0 !== count($variants)) { + $variantsIris = []; + foreach ($variants as $variant) { + $variantsIris[] = $this->sectionAwareIriConverter->getIriFromResourceInSection($variant, 'admin'); + } + $builder->withParameter('productVariants', $variantsIris); + } + + $this->client->request($builder->build()); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductOptionsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductOptionsContext.php index 0b69609561..6ab3761bb9 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductOptionsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductOptionsContext.php @@ -56,33 +56,33 @@ final class ManagingProductOptionsContext implements Context } /** - * @When I name it :name in :language + * @When I name it :name in :localeCode * @When I do not name it */ - public function iNameItInLanguage(?string $name = null, ?string $language = 'en_US'): void + public function iNameItInLanguage(?string $name = null, ?string $localeCode = 'en_US'): void { - $data = ['translations' => [$language => ['locale' => $language]]]; + $data = ['translations' => [$localeCode => []]]; if ($name !== null) { - $data['translations'][$language]['name'] = $name; + $data['translations'][$localeCode]['name'] = $name; } $this->client->updateRequestData($data); } /** - * @When I rename it to :name in :language + * @When I rename it to :name in :localeCode */ - public function iRenameItInLanguage(string $name, string $language): void + public function iRenameItInLanguage(string $name, string $localeCode): void { - $this->client->updateRequestData(['translations' => [$language => ['name' => $name, 'locale' => $language]]]); + $this->client->updateRequestData(['translations' => [$localeCode => ['name' => $name]]]); } /** - * @When I remove its name from :language translation + * @When I remove its name from :localeCode translation */ - public function iRemoveItsNameFromTranslation(string $language): void + public function iRemoveItsNameFromTranslation(string $localeCode): void { - $this->client->updateRequestData(['translations' => [$language => ['name' => '', 'locale' => $language]]]); + $this->client->updateRequestData(['translations' => [$localeCode => ['name' => '']]]); } /** @@ -103,7 +103,7 @@ final class ManagingProductOptionsContext implements Context { $this->client->addSubResourceData( 'values', - ['code' => $code, 'translations' => ['en_US' => ['value' => $value, 'locale' => 'en_US']]], + ['code' => $code, 'translations' => ['en_US' => ['value' => $value]]], ); } @@ -123,14 +123,6 @@ final class ManagingProductOptionsContext implements Context $this->client->create(); } - /** - * @When I (try to) save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @Then I should see :count product options in the list */ @@ -274,15 +266,4 @@ final class ManagingProductOptionsContext implements Context 'Product option could not be created', ); } - - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Product option could not be edited', - ); - } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductReviewsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductReviewsContext.php index 4577e84955..37fde35522 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductReviewsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductReviewsContext.php @@ -18,6 +18,7 @@ use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Review\Model\ReviewInterface; use Webmozart\Assert\Assert; @@ -38,6 +39,22 @@ final class ManagingProductReviewsContext implements Context $this->client->index(Resources::PRODUCT_REVIEWS); } + /** + * @When I choose :status as a status filter + */ + public function iChooseAsStatusFilter(string $status): void + { + $this->client->addFilter('status', $status); + } + + /** + * @When I filter + */ + public function iFilter(): void + { + $this->client->filter(); + } + /** * @When I want to modify the :productReview product review */ @@ -64,14 +81,6 @@ final class ManagingProductReviewsContext implements Context $this->client->updateRequestData(['comment' => $comment]); } - /** - * @When I (try to) save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @When I choose :rating as its rating */ @@ -189,17 +198,6 @@ final class ManagingProductReviewsContext implements Context $this->assertIfReviewHasElementWithValue($productReview, 'comment', $comment); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Product review could not be edited', - ); - } - /** * @Then I should be notified that it has been successfully deleted */ @@ -211,6 +209,20 @@ final class ManagingProductReviewsContext implements Context ); } + /** + * @Then average rating of product :product should be :expectedRating + */ + public function averageRatingOfProductShouldBe(ProductInterface $product, int $expectedRating): void + { + $averageRating = $this->responseChecker->getValue($this->client->show(Resources::PRODUCTS, (string) $product->getCode()), 'averageRating'); + + Assert::same( + $averageRating, + $expectedRating, + sprintf('Average rating of product %s is not %s', $product->getName(), $expectedRating), + ); + } + private function isItemOnIndex(string $property, string $value): bool { return $this->responseChecker->hasItemWithValue($this->client->index(Resources::PRODUCT_REVIEWS), $property, $value); diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductTaxonsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductTaxonsContext.php new file mode 100644 index 0000000000..12ce8fa30f --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductTaxonsContext.php @@ -0,0 +1,192 @@ +iAmBrowsingProductsFromTaxon($taxon); + $this->client->addFilter('page', $page); + $this->client->filter(); + + $this->sharedStorage->set('response', $this->client->getLastResponse()); + } + + /** + * @When /^I am browsing products from ("([^"]+)" taxon)$/ + */ + public function iAmBrowsingProductsFromTaxon(TaxonInterface $taxon): void + { + $this->client->index(Resources::PRODUCT_TAXONS); + $this->client->addFilter('taxon.code', $taxon->getCode()); + $this->client->addFilter('itemsPerPage', 10); + $this->client->filter(); + + $this->sharedStorage->set('response', $this->client->getLastResponse()); + } + + /** + * @When I set the position of :product to :position + */ + public function iSetThePositionOfProductTo(ProductInterface $product, int|string $position): void + { + $this->client->buildUpdateRequest(Resources::PRODUCT_TAXONS, (string) $product->getProductTaxons()->current()->getId()); + $this->client->updateRequestData(['position' => is_numeric($position) ? (int) $position : $position]); + } + + /** + * @When I assign the :taxon taxon to the :product product + * @When I (try to) add :taxon taxon to the :product product + */ + public function iAddTaxonToTheProduct(ProductInterface $product, TaxonInterface $taxon): void + { + $this->client->buildCreateRequest(Resources::PRODUCT_TAXONS); + $this->client->addRequestData('taxon', $this->iriConverter->getIriFromResource($taxon)); + $this->client->addRequestData('product', $this->iriConverter->getIriFromResource($product)); + $this->client->create(); + } + + /** + * @When I try to assign an empty taxon to the :product product + */ + public function iTryToAssignAnEmptyTaxonToTheProduct(ProductInterface $product): void + { + $this->client->buildCreateRequest(Resources::PRODUCT_TAXONS); + $this->client->addRequestData('product', $this->iriConverter->getIriFromResource($product)); + $this->client->create(); + } + + /** + * @When I try to assign an empty product to the :taxon taxon + */ + public function iTryToAssignAnEmptyProductToTheTaxon(TaxonInterface $taxon): void + { + $this->client->buildCreateRequest(Resources::PRODUCT_TAXONS); + $this->client->addRequestData('taxon', $this->iriConverter->getIriFromResource($taxon)); + $this->client->create(); + } + + /** + * @When /^I try to assign the product taxon of (product "[^"]+") and (taxon "[^"]+") to the (product "[^"]+")$/ + */ + public function iTryToAssignTheProductTaxonOfProductAndTaxonToTheProduct( + ProductInterface $productTaxonProduct, + TaxonInterface $productTaxonTaxon, + ): void { + $this->iAddTaxonToTheProduct($productTaxonProduct, $productTaxonTaxon); + } + + /** + * @When I change that the :product product does not belong to the :taxon taxon + */ + public function iChangeThatTheProductDoesNotBelongToTheTaxon( + ProductInterface $product, + TaxonInterface $taxon, + ): void { + $productTaxon = $product->getProductTaxons()->filter( + fn (ProductTaxonInterface $productTaxon) => $taxon === $productTaxon->getTaxon(), + )->first(); + + $this->client->delete(Resources::PRODUCT_TAXONS, (string) $productTaxon->getId()); + } + + /** + * @When I sort this taxon's products :order by :field + */ + public function iSortProductsBy(string $order, string $field): void + { + $this->client->sort([$field => ManagingProductsContext::SORT_TYPES[$order]]); + $this->sharedStorage->set('response', $this->client->getLastResponse()); + } + + /** + * @When I save my new configuration + */ + public function iSaveMyNewConfiguration(): void + { + $this->client->update(); + $this->sharedStorage->set('response', $this->client->getLastResponse()); + } + + /** + * @Then /^the (first|last) product on the list within this taxon should have name "([^"]+)"$/ + */ + public function theLastProductOnTheListWithinThisTaxonShouldHaveName(string $position, string $name): void + { + $productTaxons = $this->responseChecker->getCollection($this->sharedStorage->get('response')); + $productTaxon = $position === 'last' ? end($productTaxons) : reset($productTaxons); + + /** @var ProductInterface $product */ + $product = $this->iriConverter->getResourceFromIri($productTaxon['product']); + + Assert::same($product->getTranslation()->getName(), $name); + } + + /** + * @Then I should be notified that specifying a :part is required + */ + public function iShouldBeNotifiedThatSpecifyingAIsRequired(string $part): void + { + Assert::contains( + $this->client->getLastResponse()->getContent(), + sprintf('Please select a %s.', $part), + ); + } + + /** + * @Then I should be notified that product taxons cannot be duplicated + */ + public function iShouldBeNotifiedThatProductTaxonsCannotBeDuplicated(): void + { + Assert::contains( + $this->client->getLastResponse()->getContent(), + 'Product taxons cannot be duplicated.', + ); + } + + /** + * @Then I should be notified that the position :position is invalid + */ + public function iShouldBeNotifiedThatThePositionIsInvalid(): void + { + Assert::contains( + (string) $this->responseChecker->getError($this->client->getLastResponse()), + 'The type of the "position" attribute must be "int", "string" given.', + ); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php index 61f3cc1063..fe2415af42 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsContext.php @@ -13,23 +13,32 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; +use Sylius\Component\Product\Model\ProductOptionInterface; +use Sylius\Component\Product\Model\ProductOptionValueInterface; +use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; +use Sylius\Component\Shipping\Model\ShippingCategoryInterface; use Webmozart\Assert\Assert; final class ManagingProductVariantsContext implements Context { + private const FIRST_COLLECTION_ITEM = 0; + public function __construct( + private ProductVariantResolverInterface $variantResolver, private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, ) { } @@ -39,7 +48,7 @@ final class ManagingProductVariantsContext implements Context public function iWantToCreateANewProductVariant(ProductInterface $product): void { $this->client->buildCreateRequest(Resources::PRODUCT_VARIANTS); - $this->client->addRequestData('product', $this->iriConverter->getIriFromItem($product)); + $this->client->addRequestData('product', $this->iriConverter->getIriFromResource($product)); } /** @@ -50,6 +59,18 @@ final class ManagingProductVariantsContext implements Context $this->client->addRequestData('code', $code); } + /** + * @When I name it :name in :localeCode + */ + public function iNameItIn(string $name, string $localeCode): void + { + $this->client->addRequestData('translations', [ + $localeCode => [ + 'name' => $name, + ], + ]); + } + /** * @When /^I set its price to ("[^"]+") for ("[^"]+" channel)$/ */ @@ -63,6 +84,49 @@ final class ManagingProductVariantsContext implements Context ]); } + /** + * @When I remove its price from :channel channel + */ + public function iRemoveItsPriceForChannel(ChannelInterface $channel): void + { + $content = $this->client->getContent(); + $content['channelPricings'][$channel->getCode()]['price'] = null; + + $this->client->setRequestData($content); + } + + /** + * @When I do not set its price + * @When I do not specify its code + * @When I do not set its :optionName option + * @When I do not set its :firstOptionName and :secondOptionName options + */ + public function iDoNotSetValue(): void + { + // Intentionally left blank + } + + /** + * @When I do not specify its current stock + */ + public function iDoNotSpecifyItsCurrentStock(): void + { + $this->client->addRequestData('onHand', null); + } + + /** + * @When /^I set its original price to ("[^"]+") for ("[^"]+" channel)$/ + */ + public function iSetItsOriginalPriceToForChannel(int $originalPrice, ChannelInterface $channel): void + { + $this->client->addRequestData('channelPricings', [ + $channel->getCode() => [ + 'originalPrice' => $originalPrice, + 'channelCode' => $channel->getCode(), + ], + ]); + } + /** * @When /^I set its minimum price to ("[^"]+") for ("[^"]+" channel)$/ */ @@ -75,7 +139,7 @@ final class ManagingProductVariantsContext implements Context } /** - * @When I add it + * @When I( try to) add it */ public function iAddIt(): void { @@ -83,47 +147,159 @@ final class ManagingProductVariantsContext implements Context } /** - * @When /^I change the price of the ("[^"]+" product variant) to ("[^"]+") in ("[^"]+" channel)$/ + * @When I want to modify the :variant product variant */ - public function iChangeThePriceOfTheProductVariantInChannel( - ProductVariantInterface $variant, - int $price, - ChannelInterface $channel, - ): void { - $this->updateChannelPricingField($variant, $channel, $price, 'price'); + public function iWantToModifyProductVariant(ProductVariantInterface $variant): void + { + $this->client->buildUpdateRequest(Resources::PRODUCT_VARIANTS, $variant->getCode()); } /** - * @When /^I change the original price of the ("[^"]+" product variant) to ("[^"]+") in ("[^"]+" channel)$/ + * @When /^I change its price to ("[^"]+") for ("[^"]+" channel)$/ */ - public function iChangeTheOriginalPriceOfTheProductVariantInChannel( - ProductVariantInterface $variant, - int $originalPrice, - ChannelInterface $channel, - ): void { - $this->updateChannelPricingField($variant, $channel, $originalPrice, 'originalPrice'); + public function iChangeItsPriceToForChannel(int $originalPrice, ChannelInterface $channel): void + { + $this->client->addRequestData('channelPricings', [ + $channel->getCode() => [ + 'price' => $originalPrice, + 'channelCode' => $channel->getCode(), + ], + ]); } /** - * @When /^I remove the original price of the ("[^"]+" product variant) in ("[^"]+" channel)$/ + * @When I set its :optionName option to :optionValue */ - public function iRemoveTheOriginalPriceOfTheProductVariantInChannel( - ProductVariantInterface $variant, - ChannelInterface $channel, - ): void { - $this->updateChannelPricingField($variant, $channel, null, 'originalPrice'); + public function iSetItsOptionAs(string $optionName, ProductOptionValueInterface $optionValue): void + { + $content = $this->client->getContent(); + $content['optionValues'][] = $this->sectionAwareIriConverter->getIriFromResourceInSection($optionValue, 'admin'); + + $this->client->setRequestData($content); } /** - * @When /^I create a new "([^"]+)" variant priced at ("[^"]+") for ("[^"]+" product) in the ("[^"]+" channel)$/ + * @When I change its :productOption option to :productOptionValue */ - public function iCreateANewVariantPricedAtForProductInTheChannel( - string $name, - int $price, - ProductInterface $product, - ChannelInterface $channel, + public function iChangeItsOptionTo( + ProductOptionInterface $productOption, + ProductOptionValueInterface $productOptionValue, ): void { - $this->createNewVariantWithPrice($name, $price, $product, $channel); + $content = $this->client->getContent(); + foreach ($content['optionValues'] as $key => $optionValueIri) { + /** @var ProductOptionValueInterface $currentOptionValue */ + $currentOptionValue = $this->iriConverter->getResourceFromIri($optionValueIri); + if ($currentOptionValue->getOptionCode() === $productOption->getCode()) { + unset($content['optionValues'][$key]); + } + } + + $content['optionValues'][] = $this->iriConverter->getIriFromResource($productOptionValue); + + $this->client->setRequestData($content); + } + + /** + * @When I add additionally :productOptionValue value as :productOptionName option + */ + public function iAddAdditionallyValueAsOption( + ProductOptionValueInterface $productOptionValue, + string $productOptionName, + ): void { + $content = $this->client->getContent(); + $content['optionValues'][] = $this->iriConverter->getIriFromResource($productOptionValue); + + $this->client->setRequestData($content); + } + + /** + * @When I set its shipping category as :shippingCategory + */ + public function iSetItsShippingCategoryAs(ShippingCategoryInterface $shippingCategory): void + { + $this->client->addRequestData('shippingCategory', $this->iriConverter->getIriFromResource($shippingCategory)); + } + + /** + * @When I do not want to have shipping required for this product variant + */ + public function iDoNotWantToHaveShippingRequiredForThisProductVariant(): void + { + $this->client->addRequestData('shippingRequired', false); + } + + /** + * @When /^I want to view all variants of (this product)$/ + * @When /^I view(?:| all) variants of the (product "[^"]+")(?:| again)$/ + */ + public function iWantToViewAllVariantsOfThisProduct(ProductInterface $product): void + { + $this->client->index(Resources::PRODUCT_VARIANTS); + $this->client->addFilter('product', $this->iriConverter->getIriFromResource($product)); + $this->client->addFilter('order[position]', 'asc'); + $this->client->filter(); + } + + /** + * @When /^I delete the ("[^"]+" variant of product "[^"]+")$/ + * @When /^I try to delete the ("[^"]+" variant of product "[^"]+")$/ + */ + public function iDeleteTheVariantOfProduct(ProductVariantInterface $productVariant): void + { + $this->client->delete(Resources::PRODUCT_VARIANTS, $productVariant->getCode()); + } + + /** + * @When I disable it + */ + public function iDisableIt(): void + { + $this->client->updateRequestData(['enabled' => false]); + } + + /** + * @When I enable it + */ + public function iEnableIt(): void + { + $this->client->updateRequestData(['enabled' => true]); + } + + /** + * @When I disable its inventory tracking + */ + public function iDisableItsTracking(): void + { + $this->client->updateRequestData(['tracked' => false]); + } + + /** + * @When I enable its inventory tracking + */ + public function iEnableItsTracking(): void + { + $this->client->updateRequestData(['tracked' => true]); + } + + /** + * @When I set its height, width, depth and weight to :value + */ + public function iSetItsDimensionsTo(float $value): void + { + $this->client->updateRequestData([ + 'height' => $value, + 'width' => $value, + 'depth' => $value, + 'weight' => $value, + ]); + } + + /** + * @When I change its quantity of inventory to :amount + */ + public function iChangeItsQuantityOfInventoryTo(int $amount): void + { + $this->client->updateRequestData(['onHand' => $amount]); } /** @@ -133,14 +309,17 @@ final class ManagingProductVariantsContext implements Context { Assert::true( $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), - 'Product Variant could not be created', + sprintf( + 'Product Variant could not be created: %s', + $this->responseChecker->getError($this->client->getLastResponse()), + ), ); } /** - * @Then the :productVariantCode variant of the :product product should appear in the store + * @Then the :productVariantCode variant of the :productName product should appear in the store */ - public function theProductVariantShouldAppearInTheShop(string $productVariantCode, ProductInterface $product): void + public function theProductVariantShouldAppearInTheShop(string $productVariantCode, string $productName): void { $response = $this->client->index(Resources::PRODUCT_VARIANTS); @@ -148,13 +327,71 @@ final class ManagingProductVariantsContext implements Context } /** - * @Then /^the (variant with code "[^"]+") should be priced at ("[^"]+") for (channel "([^"]+)")$/ + * @Then the :productVariantCode variant of the :productName product should not appear in the store */ - public function theVariantWithCodeShouldBePricedAtForChannel(ProductVariantInterface $productVariant, int $price, ChannelInterface $channel): void + public function theProductVariantShouldNotAppearInTheShop(string $productVariantCode, string $productName): void + { + $response = $this->client->index(Resources::PRODUCT_VARIANTS); + + Assert::false($this->responseChecker->hasItemWithValue($response, 'code', $productVariantCode)); + } + + /** + * @Then /^the (?:variant with code "[^"]+") should be named "([^"]+)" in ("([^"]+)" locale)$/ + */ + public function theVariantWithCodeShouldBeNamedIn(string $name, string $localeCode): void { $response = $this->responseChecker->getCollection($this->client->index(Resources::PRODUCT_VARIANTS)); - Assert::same($response[0]['channelPricings'][$channel->getCode()]['price'], $price); + $expectedTranslation = [ + 'name' => $name, + ]; + + $translationInLocale = $response[self::FIRST_COLLECTION_ITEM]['translations'][$localeCode]; + + Assert::allInArray( + $expectedTranslation, + $translationInLocale, + sprintf('Expected translation %s, got %s', $expectedTranslation['name'], $translationInLocale['name']), + ); + } + + /** + * @Then /^the variant with code "([^"]+)" should be priced at ("[^"]+") for (channel "[^"]+")$/ + * @Then I should not have configured price for :channel channel + */ + public function theVariantWithCodeShouldBePricedAtForChannel( + ?string $variantCode = null, + ?int $price = null, + ?ChannelInterface $channel = null, + ): void { + $response = $this->responseChecker->getCollection($this->client->index(Resources::PRODUCT_VARIANTS)); + + Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['price'], $price); + } + + /** + * @Then /^the variant with code "([^"]+)" should have an original price of ("[^"]+") for (channel "[^"]+")$/ + * @Then /^the variant with code "([^"]+)" should be originally priced at ("[^"]+") for (channel "[^"]+")$/ + */ + public function theVariantWithCodeShouldHaveAnOriginalPriceOfForChannel( + ?string $variantCode, + int $originalPrice, + ChannelInterface $channel, + ): void { + $response = $this->responseChecker->getCollection($this->client->index(Resources::PRODUCT_VARIANTS)); + + Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['originalPrice'], $originalPrice); + } + + /** + * @Then /^I should have original price equal to ("[^"]+") in ("[^"]+" channel)$/ + */ + public function iShouldHaveOriginalPriceEqualToInChannel( + int $originalPrice, + ChannelInterface $channel, + ): void { + $this->theVariantWithCodeShouldHaveAnOriginalPriceOfForChannel(null, $originalPrice, $channel); } /** @@ -164,41 +401,349 @@ final class ManagingProductVariantsContext implements Context { $response = $this->responseChecker->getCollection($this->client->index(Resources::PRODUCT_VARIANTS)); - Assert::same($response[0]['channelPricings'][$channel->getCode()]['minimumPrice'], $minimumPrice); + Assert::same($response[self::FIRST_COLLECTION_ITEM]['channelPricings'][$channel->getCode()]['minimumPrice'], $minimumPrice); } - private function updateChannelPricingField( - ProductVariantInterface $variant, - ChannelInterface $channel, - ?int $price, + /** + * @Then /^the (variant with code "[^"]+") should not have shipping required$/ + */ + public function theVariantWithCodeShouldNotHaveShippingRequired(ProductVariantInterface $productVariant): void + { + Assert::false($this->responseChecker->getValue($this->client->getLastResponse(), 'shippingRequired')); + } + + /** + * @Then I should see :amount variant(s) in the list + */ + public function iShouldSeeNumberOfProductVariantsInTheList(int $amount): void + { + Assert::count($this->responseChecker->getCollection($this->client->getLastResponse()), $amount); + } + + /** + * @Then I should see that the :productVariant variant is not tracked + */ + public function iShouldSeeThatVariantIsNotTracked(ProductVariantInterface $productVariant): void + { + Assert::true($this->responseChecker->hasItemWithValues( + $this->client->getLastResponse(), + ['code' => $productVariant->getCode(), 'tracked' => false], + )); + } + + /** + * @Then I should see that the :productVariant variant has zero on hand quantity + */ + public function iShouldSeeThatTheVariantHasZeroOnHandQuantity(ProductVariantInterface $productVariant): void + { + Assert::true($this->responseChecker->hasItemWithValues( + $this->client->getLastResponse(), + ['code' => $productVariant->getCode(), 'onHand' => 0], + )); + } + + /** + * @Then I should see that the :productVariant variant is enabled + */ + public function iShouldSeeThatTheVariantIsEnabled(ProductVariantInterface $productVariant): void + { + Assert::true($this->responseChecker->hasItemWithValues( + $this->client->getLastResponse(), + ['code' => $productVariant->getCode(), 'enabled' => true], + )); + } + + /** + * @Then this variant should be disabled + */ + public function thisVariantShouldBeDisabled(): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'enabled', false)); + } + + /** + * @Then this variant should be enabled + */ + public function thisVariantShouldBeEnabled(): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'enabled', true)); + } + + /** + * @Then I should be notified that it has been successfully deleted + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyDeleted(): void + { + Assert::true( + $this->responseChecker->isDeletionSuccessful($this->client->getLastResponse()), + 'Product variant could not be deleted', + ); + } + + /** + * @Then /^(this variant) should not exist in the product catalog$/ + */ + public function thisProductVariantShouldNotExistInTheProductCatalog(ProductVariantInterface $productVariant): void + { + Assert::false( + $this->responseChecker->hasItemWithValue( + $this->client->index(Resources::PRODUCT_VARIANTS), + 'code', + $productVariant->getCode(), + ), + 'The product variant still exists, but it should not', + ); + } + + /** + * @Then /^(this variant) should still exist in the product catalog$/ + */ + public function thisProductVariantShouldStillExistInTheProductCatalog(ProductVariantInterface $productVariant): void + { + Assert::true( + $this->responseChecker->hasItemWithValue( + $this->client->index(Resources::PRODUCT_VARIANTS), + 'code', + $productVariant->getCode(), + ), + 'The product variant does not exist, but it should', + ); + } + + /** + * @Then I should be notified that this variant is in use and cannot be deleted + */ + public function iShouldBeNotifiedThatThisVariantIsInUseAndCannotBeDeleted(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Cannot delete, the product variant is in use.', + ); + } + + /** + * @Then inventory of this variant should not be tracked + */ + public function inventoryOfThisVariantShouldNotBeTracked(): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'tracked', false)); + } + + /** + * @Then inventory of this variant should be tracked + */ + public function inventoryOfThisVariantShouldBeTracked(): void + { + Assert::true($this->responseChecker->hasValue($this->client->getLastResponse(), 'tracked', true)); + } + + /** + * @Then I should be notified that prices in all channels must be defined + */ + public function iShouldBeNotifiedThatPricesInAllChannelsMustBeDefined(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'You must define price for every enabled channel.', + ); + } + + /** + * @Then I should be notified that price cannot be lower than 0 + */ + public function iShouldBeNotifiedThatPriceCannotBeLowerThanZero(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Price cannot be lower than 0.', + ); + } + + /** + * @Then I should be notified that code is required + */ + public function iShouldBeNotifiedThatCodeIsRequired(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Please enter the code.', + ); + } + + /** + * @Then I should be notified that current stock is required + */ + public function iShouldBeNotifiedThatCurrentStockIsRequired(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'The type of the "onHand" attribute must be "int", "NULL" given.', + ); + } + + /** + * @Then the :product product should have no variants + */ + public function theProductShouldHaveNoVariants(ProductInterface $product): void + { + $this->iWantToViewAllVariantsOfThisProduct($product); + $this->iShouldSeeNumberOfProductVariantsInTheList(0); + } + + /** + * @Then the :product product should have only one variant + */ + public function theProductShouldHaveOnlyOneVariant(ProductInterface $product): void + { + $this->iWantToViewAllVariantsOfThisProduct($product); + $this->iShouldSeeNumberOfProductVariantsInTheList(1); + } + + /** + * @Then /^(\d+) units of (this product) should be (on hand|on hold)$/ + */ + public function unitsOfThisProductShouldBeOn( + int $quantity, + ProductInterface $product, string $field, ): void { - $this->client->buildUpdateRequest(Resources::PRODUCT_VARIANTS, $variant->getCode()); + /** @var ProductVariantInterface $variant */ + $variant = $this->variantResolver->getVariant($product); + Assert::isInstanceOf($variant, ProductVariantInterface::class); - $content = $this->client->getContent(); - $content['channelPricings'][$channel->getCode()][$field] = $price; - $this->client->updateRequestData($content); - - $this->client->update(); + $this->iWantToViewAllVariantsOfThisProduct($product); + $this->theVariantShouldHaveItemsOn($variant, $quantity, $field); } - private function createNewVariantWithPrice( - string $name, - int $price, - ProductInterface $product, - ChannelInterface $channel, + /** + * @Then /^there should be no units of (this product) on hold$/ + */ + public function thereShouldBeNoUnitsOfThisProductOnHold(ProductInterface $product): void + { + $this->unitsOfThisProductShouldBeOn(0, $product, 'on hold'); + } + + /** + * @Then /^the ("[^"]+" variant) should have (\d+) items (on hand|on hold)$/ + * @Then /^the (variant "[^"]+") should have (\d+) items (on hand|on hold)$/ + */ + public function theVariantShouldHaveItemsOn(ProductVariantInterface $variant, int $quantity, string $field): void + { + $variantsData = $this->responseChecker->getCollectionItemsWithValue( + $this->client->getLastResponse(), + 'code', + $variant->getCode(), + ); + + $variantData = array_pop($variantsData); + + Assert::same( + (int) $variantData[StringInflector::nameToCamelCase($field)], + $quantity, + ); + } + + /** + * @Then /^the ("[^"]+" variant of product "[^"]+") should have (\d+) items (on hand|on hold)$/ + * @Then /^the ("[^"]+" variant of "[^"]+" product) should have (\d+) items (on hand|on hold)$/ + * @Then /^(this variant) should have a (\d+) item currently in stock$/ + */ + public function theVariantOfProductShouldHaveItemsOn( + ProductVariantInterface $variant, + int $quantity, + string $field = 'on hand', ): void { - $this->client->buildCreateRequest(Resources::PRODUCT_VARIANTS); - $this->client->addRequestData('product', $this->iriConverter->getIriFromItem($product)); - $this->client->addRequestData('code', StringInflector::nameToCode($name)); + $actualQuantity = $this->responseChecker->getValue( + $this->client->show(Resources::PRODUCT_VARIANTS, $variant->getCode()), + StringInflector::nameToCamelCase($field), + ); - $this->client->addRequestData('channelPricings', [ - $channel->getCode() => [ - 'price' => $price, - 'channelCode' => $channel->getCode(), - ], - ]); + Assert::same( + (int) $actualQuantity, + $quantity, + ); + } - $this->client->create(); + /** + * @Then I should be notified that code has to be unique + */ + public function iShouldBeNotifiedThatCodeHasToBeUnique(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Product variant code must be unique.', + ); + } + + /** + * @Then I should be notified that this variant already exists + */ + public function iShouldBeNotifiedThatThisVariantAlreadyExists(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Variant with this option set already exists.', + ); + } + + /** + * @Then I should be notified that height, width, depth and weight cannot be lower than 0 + */ + public function iShouldBeNotifiedThatIsHeightWidthDepthAndWeightCannotBeLowerThanZero(): void + { + $errors = $this->responseChecker->getError($this->client->getLastResponse()); + + Assert::contains($errors, 'Height cannot be negative.'); + Assert::contains($errors, 'Width cannot be negative.'); + Assert::contains($errors, 'Depth cannot be negative.'); + Assert::contains($errors, 'Weight cannot be negative.'); + } + + /** + * @Then the variant :productVariantName should have :optionName option as :optionValue + */ + public function theVariantShouldHaveOptionAs( + string $productVariantName, + string $optionName, + ProductOptionValueInterface $optionValue, + ): void { + Assert::true($this->responseChecker->hasValueInCollection( + $this->client->getLastResponse(), + 'optionValues', + $this->sectionAwareIriConverter->getIriFromResourceInSection($optionValue, 'admin'), + )); + } + + /** + * @Then I should be notified that the variant can have only one value configured for a single option + */ + public function iShouldBeNotifiedThatTheVariantCanHaveOnlyOneValueConfiguredForASingleOption(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'The product variant can have only one value configured for a single option.', + ); + } + + /** + * @Then I should be notified that required options have not been configured + */ + public function iShouldBeNotifiedThatRequiredOptionsHaveNotBeenConfigured(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'The product variant must have configured values for all options chosen on the product.', + ); + } + + /** + * @Then I should be notified that on hand quantity must be greater than the number of on hold units + */ + public function iShouldBeNotifiedThatOnHandQuantityMustBeGreaterThanTheNumberOfOnHoldUnits(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'On hand must be greater than the number of on hold units', + ); } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsPricesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsPricesContext.php new file mode 100644 index 0000000000..f5ddf9133e --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductVariantsPricesContext.php @@ -0,0 +1,74 @@ +updateChannelPricingField($variant, $channel, $price, 'price'); + } + + /** + * @When /^I change the original price of the ("[^"]+" product variant) to ("[^"]+") in ("[^"]+" channel)$/ + */ + public function iChangeTheOriginalPriceOfTheProductVariantInChannel( + ProductVariantInterface $variant, + int $originalPrice, + ChannelInterface $channel, + ): void { + $this->updateChannelPricingField($variant, $channel, $originalPrice, 'originalPrice'); + } + + /** + * @When /^I remove the original price of the ("[^"]+" product variant) in ("[^"]+" channel)$/ + */ + public function iRemoveTheOriginalPriceOfTheProductVariantInChannel( + ProductVariantInterface $variant, + ChannelInterface $channel, + ): void { + $this->updateChannelPricingField($variant, $channel, null, 'originalPrice'); + } + + private function updateChannelPricingField( + ProductVariantInterface $variant, + ChannelInterface $channel, + ?int $price, + string $field, + ): void { + $this->client->buildUpdateRequest(Resources::PRODUCT_VARIANTS, $variant->getCode()); + + $content = $this->client->getContent(); + $content['channelPricings'][$channel->getCode()][$field] = $price; + $this->client->updateRequestData($content); + + $this->client->update(); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php index feb970e803..1b531d3448 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingProductsContext.php @@ -13,16 +13,24 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Component\Attribute\Model\AttributeValueInterface; use Sylius\Component\Core\Model\AdminUserInterface; +use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Model\ProductTaxonInterface; +use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Model\TaxonInterface; -use Sylius\Component\Product\Model\ProductOption; +use Sylius\Component\Locale\Model\LocaleInterface; +use Sylius\Component\Product\Model\ProductAssociationInterface; +use Sylius\Component\Product\Model\ProductAssociationTypeInterface; +use Sylius\Component\Product\Model\ProductAttributeInterface; use Sylius\Component\Product\Model\ProductOptionInterface; use Symfony\Component\HttpFoundation\Response; use Webmozart\Assert\Assert; @@ -35,6 +43,7 @@ final class ManagingProductsContext implements Context private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, private SharedStorageInterface $sharedStorage, private string $apiUrlPrefix, ) { @@ -106,7 +115,7 @@ final class ManagingProductsContext implements Context */ public function iRenameItToIn(?string $name = null, string $localeCode = 'en_US'): void { - $data['translations'][$localeCode]['locale'] = $localeCode; + $data['translations'][$localeCode] = []; if ($name !== null) { $data['translations'][$localeCode]['name'] = $name; @@ -125,7 +134,6 @@ final class ManagingProductsContext implements Context $data = [ 'translations' => [ $localeCode => [ - 'locale' => $localeCode, 'slug' => $slug, ], ], @@ -145,16 +153,9 @@ final class ManagingProductsContext implements Context /** * @When I add the :productOption option to it */ - public function iAddTheOptionToIt(ProductOption $productOption): void + public function iAddTheOptionToIt(ProductOptionInterface $productOption): void { - /** @var ProductInterface $product */ - $product = $this->sharedStorage->get('product'); - - $productOptions = $this->responseChecker->getValue($this->client->show(Resources::PRODUCTS, $product->getCode()), 'options'); - - $productOptions[] = $this->iriConverter->getIriFromItemInSection($productOption, 'admin'); - - $this->client->updateRequestData(['options' => $productOptions]); + $this->client->updateRequestData(['options' => [$this->sectionAwareIriConverter->getIriFromResourceInSection($productOption, 'admin')]]); } /** @@ -162,15 +163,7 @@ final class ManagingProductsContext implements Context */ public function iChooseMainTaxon(TaxonInterface $taxon): void { - $this->client->updateRequestData(['mainTaxon' => $this->iriConverter->getIriFromItemInSection($taxon, 'admin')]); - } - - /** - * @When I (try to) save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); + $this->client->updateRequestData(['mainTaxon' => $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin')]); } /** @@ -245,7 +238,6 @@ final class ManagingProductsContext implements Context 'translations' => [ $localeCode => [ 'name' => '', - 'locale' => $localeCode, ], ], ]); @@ -260,7 +252,6 @@ final class ManagingProductsContext implements Context 'translations' => [ $localeCode => [ 'metaKeywords' => str_repeat('a', 256), - 'locale' => $localeCode, ], ], ]); @@ -275,12 +266,283 @@ final class ManagingProductsContext implements Context 'translations' => [ $localeCode => [ 'metaDescription' => str_repeat('a', 256), - 'locale' => $localeCode, ], ], ]); } + /** + * @When I set its non-translatable :attribute attribute to :value + */ + public function iSetItsNonTranslatableAttributeTo(ProductAttributeInterface $attribute, string $value): void + { + $this->client->addSubResourceData( + 'attributes', + [ + 'attribute' => $this->iriConverter->getIriFromResource($attribute), + 'value' => $this->getAttributeValueInProperType($attribute, $value), + ], + ); + } + + /** + * @When I set the invalid integer value of the non-translatable :attribute attribute to :value + */ + public function iSetTheInvalidIntegerValueOfTheNonTranslatableAttributeTo(ProductAttributeInterface $attribute, int $value): void + { + $this->client->addSubResourceData( + 'attributes', + [ + 'attribute' => $this->iriConverter->getIriFromResource($attribute), + 'value' => $value, + ], + ); + } + + /** + * @When I set the invalid string value of the non-translatable :attribute attribute to :value + */ + public function iSetTheInvalidStringValueOfTheNonTranslatableAttributeTo(ProductAttributeInterface $attribute, string $value): void + { + $this->client->addSubResourceData( + 'attributes', + [ + 'attribute' => $this->iriConverter->getIriFromResource($attribute), + 'value' => $value, + ], + ); + } + + /** + * @When I set its :attribute attribute to :value + * @When I set its :attribute attribute to :value in :localeCode + * @When I do not set its :attribute attribute in :localeCode + */ + public function iSetItsAttributeTo( + ProductAttributeInterface $attribute, + ?string $value = null, + string $localeCode = 'en_US', + ): void { + $this->client->addSubResourceData( + 'attributes', + [ + 'attribute' => $this->sectionAwareIriConverter->getIriFromResourceInSection($attribute, 'admin'), + 'value' => $value !== null ? $this->getAttributeValueInProperType($attribute, $value) : null, + 'localeCode' => $localeCode, + ], + ); + } + + /** + * @When I remove its :attribute attribute + */ + public function iRemoveItsAttribute(ProductAttributeInterface $attribute): void + { + $attributeIri = $this->sectionAwareIriConverter->getIriFromResourceInSection($attribute, 'admin'); + + $content = $this->client->getContent(); + foreach ($content['attributes'] as $key => $attributeValue) { + if ($attributeValue['attribute'] === $attributeIri) { + unset($content['attributes'][$key]); + } + } + + $this->client->setRequestData($content); + } + + /** + * @When I add the :attributeName attribute + */ + public function iAddTheAttribute(string $attributeName): void + { + // Intentionally left blank + } + + /** + * @When I select :value value in :localeCode for the :attribute attribute + */ + public function iSelectValueInForTheAttribute( + string $value, + string $localeCode, + ProductAttributeInterface $attribute, + ): void { + $this->client->addSubResourceData( + 'attributes', + [ + 'attribute' => $this->iriConverter->getIriFromResource($attribute), + 'value' => [$this->getSelectAttributeValueUuidByChoiceValue($attribute, $value)], + 'localeCode' => $localeCode, + ], + ); + } + + /** + * @When I select :value value for the :attribute attribute + */ + public function iSelectValueForTheAttribute( + string $value, + ProductAttributeInterface $attribute, + ): void { + $this->client->addSubResourceData( + 'attributes', + [ + 'attribute' => $this->iriConverter->getIriFromResource($attribute), + 'value' => [$this->getSelectAttributeValueUuidByChoiceValue($attribute, $value)], + ], + ); + } + + /** + * @When I assign it to channel :channel + */ + public function iAssignItToChannel(ChannelInterface $channel): void + { + $this->client->addRequestData('channels', [$this->iriConverter->getIriFromResource($channel)]); + } + + /** + * @When I access the :product product + */ + public function iAccessTheProduct(ProductInterface $product): void + { + $this->client->show(Resources::PRODUCTS, $product->getCode()); + } + + /** + * @When I choose :channel as a channel filter + */ + public function iChooseChannelAsAChannelFilter(ChannelInterface $channel): void + { + $this->client->addFilter('channel', $this->iriConverter->getIriFromResource($channel)); + } + + /** + * @When I save my changes to the images + */ + public function iSaveMyChangesToTheImages(): void + { + // Intentionally left blank + } + + /** + * @When I filter + */ + public function iFilter(): void + { + $this->client->filter(); + + $this->sharedStorage->set('response', $this->client->getLastResponse()); + } + + /** + * @Then I should see main taxon is :taxon + */ + public function iShouldSeeMainTaxonIs(TaxonInterface $taxon): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'mainTaxon'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin'), + ); + } + + /** + * @Then I should see product taxon :taxon + */ + public function iShouldSeeProductTaxon(TaxonInterface $taxon): void + { + $product = $this->sharedStorage->get('product'); + Assert::isInstanceOf($product, ProductInterface::class); + $productTaxon = $product->getProductTaxons()->filter( + fn (ProductTaxonInterface $productTaxon) => $productTaxon->getTaxon()->getCode() === $taxon->getCode(), + )->first(); + Assert::isInstanceOf($productTaxon, ProductTaxonInterface::class); + + Assert::true($this->responseChecker->hasValueInCollection( + $this->client->getLastResponse(), + 'productTaxons', + $this->sectionAwareIriConverter->getIriFromResourceInSection($productTaxon, 'admin'), + )); + } + + /** + * @Then I should see option :productOption + */ + public function iShouldSeeOption(ProductOptionInterface $productOption): void + { + Assert::true($this->responseChecker->hasValueInCollection( + $this->client->getLastResponse(), + 'options', + $this->sectionAwareIriConverter->getIriFromResourceInSection($productOption, 'admin'), + )); + } + + /** + * @Then I should see :count variants + */ + public function iShouldSeeVariants(int $count): void + { + Assert::count( + $this->responseChecker->getResponseContent($this->client->getLastResponse())['variants'] ?? [], + $count, + ); + } + + /** + * @Then I should see the :variant variant + */ + public function iShouldSeeTheVariant(ProductVariantInterface $variant): void + { + Assert::true($this->responseChecker->hasValueInCollection( + $this->client->getLastResponse(), + 'variants', + $this->sectionAwareIriConverter->getIriFromResourceInSection($variant, 'admin'), + )); + } + + /** + * @Then I should see product :field is :value + * @Then I should see product's :field is :value + */ + public function iShouldSeeProductFieldIs(string $field, string $value): void + { + $this->assertResponseHasTranslationFieldWithValue($field, $value); + } + + /** + * @Then I should see product's meta keyword(s) is/are :metaKeywords + */ + public function iShouldSeeProductMetaKeywordsAre(string $metaKeywords): void + { + $this->assertResponseHasTranslationFieldWithValue('metaKeywords', $metaKeywords); + } + + /** + * @Then I should see product's short description is :shortDescription + */ + public function iShouldSeeProductShortDescriptionIs(string $shortDescription): void + { + $this->assertResponseHasTranslationFieldWithValue('shortDescription', $shortDescription); + } + + /** + * @Then I should see product association type :productAssociationType + */ + public function iShouldSeeProductAssociationType(ProductAssociationTypeInterface $productAssociationType): void + { + $associations = $this->responseChecker->getValue($this->client->getLastResponse(), 'associations'); + foreach ($associations as $associationIri) { + /** @var ProductAssociationInterface $association */ + $association = $this->iriConverter->getResourceFromIri($associationIri); + if ($association->getType()->getCode() === $productAssociationType->getCode()) { + return; + } + } + + throw new \InvalidArgumentException( + sprintf('Product association type "%s" not found.', $productAssociationType->getCode()), + ); + } + /** * @Then I should be notified that it has been successfully created */ @@ -289,17 +551,6 @@ final class ManagingProductsContext implements Context Assert::true($this->responseChecker->isCreationSuccessful($this->client->getLastResponse())); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Product could not be edited', - ); - } - /** * @Then I should be notified that this product is in use and cannot be deleted */ @@ -367,9 +618,10 @@ final class ManagingProductsContext implements Context } /** + * @Then I should see a single product in the list * @Then I should see :count products in the list */ - public function iShouldSeeProductsInTheList(int $count): void + public function iShouldSeeProductsInTheList(int $count = 1): void { Assert::count($this->responseChecker->getCollection($this->client->getLastResponse()), $count); } @@ -430,7 +682,21 @@ final class ManagingProductsContext implements Context $mainTaxon = $this->responseChecker->getValue($response, 'mainTaxon'); - Assert::same($mainTaxon, $this->iriConverter->getIriFromItemInSection($taxon, 'admin')); + Assert::same($mainTaxon, $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin')); + } + + /** + * @Then the product :product should have the :taxon taxon + */ + public function thisProductTaxonShouldBe(ProductInterface $product, TaxonInterface $taxon): void + { + $this->client->index(Resources::PRODUCT_TAXONS); + Assert::true( + $this->responseChecker->hasItemWithValues($this->client->getLastResponse(), [ + 'product' => $this->sectionAwareIriConverter->getIriFromResourceInSection($product, 'admin'), + 'taxon' => $this->sectionAwareIriConverter->getIriFromResourceInSection($taxon, 'admin'), + ]), + ); } /** @@ -469,7 +735,7 @@ final class ManagingProductsContext implements Context $productFromResponse = $this->responseChecker->getResponseContent($response); Assert::true( - in_array($this->iriConverter->getIriFromItemInSection($productOption, 'admin'), $productFromResponse['options'], true), + in_array($this->sectionAwareIriConverter->getIriFromResourceInSection($productOption, 'admin'), $productFromResponse['options'], true), sprintf('Product with option %s does not exist', $productOption->getName()), ); } @@ -479,16 +745,37 @@ final class ManagingProductsContext implements Context */ public function theFirstProductOnTheListShouldHave(string $field, string $value): void { - $response = $this->getLastResponse(); + $products = $this->responseChecker->getCollection($this->getLastResponse()); - $products = $this->responseChecker->getCollection($response); + Assert::same($this->getFieldValueOfProduct($products[0], $field), $value); + } - Assert::same($this->getFieldValueOfFirstProduct($products[0], $field), $value); + /** + * @Then the last product on the list should have name :name + */ + public function theLastProductOnTheListShouldHaveName(string $name): void + { + $products = $this->responseChecker->getCollection($this->getLastResponse()); + + Assert::same($this->getFieldValueOfProduct(end($products), 'name'), $name); + } + + /** + * @Then /^the (first|last) product on the list shouldn't have a name$/ + */ + public function theProductOnTheListShouldNotHaveAName(string $position): void + { + $products = $this->responseChecker->getCollection($this->getLastResponse()); + + $product = $position === 'last' ? end($products) : reset($products); + + Assert::null($this->getFieldValueOfProduct($product, 'name')); } /** * @Then /^the slug of the ("[^"]+" product) should(?:| still) be "([^"]+)"$/ * @Then /^the slug of the ("[^"]+" product) should(?:| still) be "([^"]+)" (in the "[^"]+" locale)$/ + * @Then /^(this product) should(?:| still) have slug "([^"]+)" in ("[^"]+" locale)$/ */ public function productSlugShouldBe(ProductInterface $product, string $slug, $localeCode = 'en_US'): void { @@ -511,7 +798,7 @@ final class ManagingProductsContext implements Context $this->responseChecker->getCollectionItemsWithValue( $response, 'reviewSubject', - $this->iriConverter->getIriFromItemInSection($product, 'admin'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($product, 'admin'), ), 'Should be no reviews, but some exist', ); @@ -549,6 +836,154 @@ final class ManagingProductsContext implements Context Assert::false($this->hasProductWithFieldValue($this->client->index(Resources::PRODUCTS), $field, $value)); } + /** + * @Then non-translatable attribute :attribute of product :product should be :value + * @Then select attribute :attribute of product :product should be :value + */ + public function nonTranslatableAttributeOfProductShouldBe( + ProductAttributeInterface $attribute, + ProductInterface $product, + string $value, + ): void { + $this->client->show(Resources::PRODUCTS, $product->getCode()); + + $this->hasAttributeWithValueInLastResponse($attribute, $value); + } + + /** + * @Then I should see non-translatable attribute :attribute with value :value% + */ + public function iShouldSeeNonTranslatableAttributeWithValue(ProductAttributeInterface $attribute, int $value): void + { + $this->hasAttributeWithValueInLastResponse($attribute, (string) ($value / 100)); + } + + /** + * @Then attribute :attribute of product :product should be :value + * @Then attribute :attribute of product :product should be :value in :localeCode + * @Then select attribute :attribute of product :product should be :value in :localeCode + */ + public function attributeOfProductShouldBe( + ProductAttributeInterface $attribute, + ProductInterface $product, + string $value, + string $localeCode = 'en_US', + ): void { + $this->client->show(Resources::PRODUCTS, $product->getCode()); + + $this->hasAttributeWithValueInLastResponse($attribute, $value, $localeCode); + } + + /** + * @Then product :product should not have a :attribute attribute + */ + public function productShouldNotHaveAttribute(ProductInterface $product, ProductAttributeInterface $attribute): void + { + $attributes = $this->responseChecker->getValue($this->client->getLastResponse(), 'attributes'); + foreach ($attributes as $attributeValue) { + if ($attributeValue['attribute'] === $this->sectionAwareIriConverter->getIriFromResourceInSection($attribute, 'admin')) { + throw new \InvalidArgumentException( + sprintf('Product %s have attribute %s', $product->getName(), $attribute->getName()), + ); + } + } + } + + /** + * @Then I should not be able to edit its options + */ + public function iShouldNotBeAbleToEditItsOptions(): void + { + $productOption = $this->sharedStorage->get('product_option'); + $productOptionIri = $this->sectionAwareIriConverter->getIriFromResourceInSection($productOption, 'admin'); + $this->client->updateRequestData(['options' => [$productOptionIri]]); + + $res = $this->client->update(); + + Assert::false( + $this->responseChecker->hasValueInCollection($res, 'options', $productOptionIri), + 'The product options should not be changed, but they were', + ); + } + + /** + * @Then I should be notified that I have to define product variants' prices for newly assigned channels first + */ + public function iShouldBeNotifiedThatIHaveToDefineProductVariantsPricesForNewlyAssignedChannelsFirst(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'You have to define product variants\' prices for newly assigned channels first.', + ); + } + + /** + * @Then I should be notified that slug has to be unique + */ + public function iShouldBeNotifiedThatSlugHasToBeUnique(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Product slug must be unique.', + ); + } + + /** + * @Then I should be notified that I have to define the :attributeName attribute in :localeCode + */ + public function iShouldBeNotifiedThatIHaveToDefineTheAttributeIn(string $attributeName, string $localeCode): void + { + Assert::regex( + $this->responseChecker->getError($this->client->getLastResponse()), + '/attributes\[[\d+]\]\.value: This value should not be blank\./', + ); + } + + /** + * @Then I should be notified that the :attributeName attribute in :localeCode should be longer than :number + */ + public function iShouldBeNotifiedThatTheAttributeInShouldBeLongerThan( + string $attributeName, + string $localeCode, + int $number, + ): void { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('This value is too short. It should have %s characters or more.', $number), + ); + } + + /** + * @Then I should be notified that the value of the :attributeName attribute has invalid type + */ + public function iShouldBeNotifiedThatTheValueOfTheAttributeHasInvalidType( + string $attributeName, + ): void { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('The value of attribute "%s" has an invalid type', $attributeName), + ); + } + + /** + * @Then I should see an image related to this product + */ + public function iShouldSeeImageRelatedToThisProduct(): void + { + Assert::notEmpty($this->responseChecker->getValue($this->client->getLastResponse(), 'images')); + } + + /** + * @Then I should see attribute :attribute with value :value in :locale locale + */ + public function iShouldSeeAttributeWithValueInLocale( + ProductAttributeInterface $attribute, + string $value, + LocaleInterface $locale, + ): void { + $this->hasAttributeWithValueInLastResponse($attribute, $value, $locale->getCode()); + } + private function getAdminLocaleCode(): string { /** @var AdminUserInterface $adminUser */ @@ -559,14 +994,14 @@ final class ManagingProductsContext implements Context return $this->responseChecker->getValue($response, 'localeCode'); } - private function getFieldValueOfFirstProduct(array $product, string $field): ?string + private function getFieldValueOfProduct(array $product, string $field): ?string { if ($field === 'code') { return $product['code']; } if ($field === 'name') { - return $product['translations'][$this->getAdminLocaleCode()]['name']; + return $product['translations'][$this->getAdminLocaleCode()]['name'] ?? null; } return null; @@ -599,4 +1034,76 @@ final class ManagingProductsContext implements Context { return $this->sharedStorage->has('response') ? $this->sharedStorage->get('response') : $this->client->getLastResponse(); } + + private function getAttributeValueInProperType( + ProductAttributeInterface $productAttribute, + string $value, + ): bool|float|int|string { + switch ($productAttribute->getStorageType()) { + case AttributeValueInterface::STORAGE_BOOLEAN: + return (bool) $value; + case AttributeValueInterface::STORAGE_FLOAT: + return (float) $value; + case AttributeValueInterface::STORAGE_INTEGER: + return (int) $value; + } + + return $value; + } + + private function getSelectAttributeValueUuidByChoiceValue( + ProductAttributeInterface $attribute, + string $value, + ): string { + $choices = $attribute->getConfiguration()['choices'] ?? []; + foreach ($choices as $uuid => $choice) { + if (in_array($value, $choice, true)) { + return $uuid; + } + } + + throw new \InvalidArgumentException( + sprintf('Value "%s" not found in attribute "%s"', $value, $attribute->getName()), + ); + } + + private function hasAttributeWithValueInLastResponse( + ProductAttributeInterface $attribute, + string $value, + ?string $localeCode = null, + ): void { + $attributeIri = $this->sectionAwareIriConverter->getIriFromResourceInSection($attribute, 'admin'); + + $attributes = $this->responseChecker->getValue($this->client->getLastResponse(), 'attributes'); + foreach ($attributes as $attributeValue) { + if ($attributeValue['attribute'] === $attributeIri && $attributeValue['localeCode'] === $localeCode) { + $this->assertAttributeValue($value, $attributeValue['value']); + + return; + } + } + + throw new \InvalidArgumentException( + sprintf('The given product does not have attribute %s', $attribute->getName()), + ); + } + + private function assertAttributeValue(string $expectedValue, $value): void + { + if (is_array($value)) { + Assert::allInArray($value, [$expectedValue]); + + return; + } + + Assert::same((string) $value, $expectedValue); + } + + private function assertResponseHasTranslationFieldWithValue(string $field, string $value): void + { + Assert::same( + $this->responseChecker->getTranslationValue($this->client->getLastResponse(), $field), + $value, + ); + } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionCouponsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionCouponsContext.php new file mode 100644 index 0000000000..1f82672be4 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionCouponsContext.php @@ -0,0 +1,636 @@ +client->index(Resources::PROMOTION_COUPONS); + $this->client->addFilter( + 'promotion', + $this->sectionAwareIriConverter->getIriFromResourceInSection($promotion, 'admin'), + ); + $this->client->filter(); + } + + /** + * @When I want to create a new coupon + */ + public function iWantToCreateANewCoupon(): void + { + $this->client->buildCreateRequest(Resources::PROMOTION_COUPONS); + } + + /** + * @When /^I want to create a new coupon for (this promotion)$/ + */ + public function iWantToCreateANewCouponForPromotion(PromotionInterface $promotion): void + { + $this->client->buildCreateRequest(Resources::PROMOTION_COUPONS); + $this->client->addRequestData( + 'promotion', + $this->sectionAwareIriConverter->getIriFromResourceInSection($promotion, 'admin'), + ); + } + + /** + * @When I want to modify the :coupon coupon for this promotion + */ + public function iWantToModifyTheCouponOfThisPromotion(PromotionCouponInterface $coupon): void + { + $this->client->buildUpdateRequest(Resources::PROMOTION_COUPONS, $coupon->getCode()); + } + + /** + * @When /^I want to generate new coupons for (this promotion)$/ + */ + public function iWantToGenerateNewCouponsForThisPromotion(PromotionInterface $promotion): void + { + $this->request = $this->requestFactory->create('admin', 'promotion-coupons/generate', 'Bearer'); + $this->request->updateContent(['promotionCode' => $promotion->getCode()]); + } + + /** + * @When I (try to) delete :coupon coupon related to this promotion + */ + public function iDeleteCouponRelatedToThisPromotion(PromotionCouponInterface $coupon): void + { + $this->client->delete(Resources::PROMOTION_COUPONS, $coupon->getCode()); + } + + /** + * @When I specify its code as :code + */ + public function iSpecifyItsCodeAs(string $code): void + { + $this->client->addRequestData('code', $code); + } + + /** + * @When I limit its usage to :times time(s) + * @When I change its usage limit to :times + */ + public function iLimitItsUsageToTimes(int $times): void + { + $this->client->addRequestData('usageLimit', $times); + } + + /** + * @When I limit its per customer usage to :times time(s) + * @When I change its per customer usage limit to :times + */ + public function iLimitItsPerCustomerUsageToTimes(int $times): void + { + $this->client->addRequestData('perCustomerUsageLimit', $times); + } + + /** + * @When I make it valid until :date + * @When I change its expiration date to :date + */ + public function iMakeItValidUntil(\DateTime $date): void + { + $this->client->addRequestData('expiresAt', $date->format('d-m-Y')); + } + + /** + * @When I make it not reusable from cancelled orders + */ + public function iMakeItNotReusableFromCancelledOrders(): void + { + $this->client->addRequestData('reusableFromCancelledOrders', false); + } + + /** + * @When I choose the amount of :amount coupons to be generated + */ + public function iSpecifyItsAmountAs(int $amount): void + { + $this->request->updateContent(['amount' => $amount]); + } + + /** + * @When I specify their prefix as :prefix + */ + public function iSpecifyPrefixAs(string $prefix): void + { + $this->request->updateContent(['prefix' => $prefix]); + } + + /** + * @When I specify their suffix as :suffix + */ + public function iSpecifySuffixAs(string $suffix): void + { + $this->request->updateContent(['suffix' => $suffix]); + } + + /** + * @When /^I specify their code length as (\d+)$/ + * @When I do not specify their code length + */ + public function iSpecifyTheirCodeLengthAs(?int $codeLength = null): void + { + $this->request->updateContent(['codeLength' => $codeLength]); + } + + /** + * @When /^I limit generated coupons usage to (\d+) times?$/ + */ + public function iSetGeneratedCouponsUsageLimitTo(int $limit): void + { + $this->request->updateContent(['usageLimit' => $limit]); + } + + /** + * @When I make generated coupons valid until :date + */ + public function iMakeGeneratedCouponsValidUntil(\DateTimeInterface $date): void + { + $this->request->updateContent(['expiresAt' => $date->format('Y-m-d')]); + } + + /** + * @When I do not specify its :field + */ + public function iDoNotSpecifyIts(): void + { + // Intentionally left blank + } + + /** + * @When I (try to) add it + */ + public function iAddIt(): void + { + $this->client->create(); + } + + /** + * @When I (try to) generate these coupons + */ + public function iGenerateTheseCoupons(): void + { + $this->client->executeCustomRequest($this->request); + } + + /** + * @When /^I sort coupons by (ascending|descending) number of uses$/ + */ + public function iSortCouponsByNumberOfUses(string $order): void + { + $this->sortBy($order, 'used'); + } + + /** + * @When /^I sort coupons by (ascending|descending) code$/ + */ + public function iSortCouponsByCode(string $order): void + { + $this->sortBy($order, 'code'); + } + + /** + * @When /^I sort coupons by (ascending|descending) usage limit$/ + */ + public function iSortCouponsByUsageLimit(string $order): void + { + $this->sortBy($order, 'usageLimit'); + } + + /** + * @When /^I sort coupons by (ascending|descending) usage limit per customer$/ + */ + public function iSortCouponsByPerCustomerUsageLimit(string $order): void + { + $this->sortBy($order, 'perCustomerUsageLimit'); + } + + /** + * @When /^I sort coupons by (ascending|descending) expiration date$/ + */ + public function iSortCouponsByExpirationDate(string $order): void + { + $this->sortBy($order, 'expiresAt'); + } + + /** + * @Then /^there should(?:| still) be (\d+) coupons? related to (this promotion)$/ + */ + public function thereShouldBeCountCouponsRelatedToThisPromotion(int $count, PromotionInterface $promotion): void + { + $coupons = $this->responseChecker->getCollection( + $this->client->subResourceIndex(Resources::PROMOTIONS, Subresources::PROMOTION_COUPONS, $promotion->getCode()), + ); + Assert::same(count($coupons), $count); + } + + /** + * @Then there should be a :promotion promotion with a coupon code :code + */ + public function thereShouldBeACouponWithCode(PromotionInterface $promotion, string $code): void + { + Assert::true($this->responseChecker->hasItemWithValue( + $this->client->subResourceIndex(Resources::PROMOTIONS, Subresources::PROMOTION_COUPONS, $promotion->getCode()), + 'code', + $code, + )); + } + + /** + * @Then there should be no coupon with code :code + */ + public function thereShouldBeNoCouponWithCode(string $code): void + { + Assert::false($this->responseChecker->hasItemWithValue( + $this->client->index(Resources::PROMOTION_COUPONS), + 'code', + $code, + )); + } + + /** + * @Then I should see :count coupons on the list + */ + public function iShouldSeeCountCouponsOnTheList(int $count): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $count); + } + + /** + * @Then the first coupon should have code :code + */ + public function theFirstCouponShouldHaveCode(string $code): void + { + Assert::true($this->responseChecker->hasItemOnPositionWithValue( + $this->client->getLastResponse(), + 0, + 'code', + $code, + )); + } + + /** + * @Then I should be notified that it has been successfully created + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyCreated(): void + { + Assert::true( + $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), + 'Promotion coupon could not be created', + ); + } + + /** + * @Then this coupon should be valid until :date + */ + public function thisCouponShouldBeValidUntil(\DateTime $date): void + { + $actualDate = \DateTime::createFromFormat( + 'Y-m-d h:i:s', + $this->responseChecker->getValue($this->client->getLastResponse(), 'expiresAt'), + ); + + Assert::same( + $actualDate->format('Y-m-d'), + $date->format('Y-m-d'), + ); + } + + /** + * @Then this coupon should have :limit usage limit + */ + public function thisCouponShouldHaveUsageLimit(int $limit): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'usageLimit'), + $limit, + ); + } + + /** + * @Then this coupon should have :limit per customer usage limit + */ + public function thisCouponShouldHavePerCustomerUsageLimit(int $limit): void + { + Assert::same( + $this->responseChecker->getValue($this->client->getLastResponse(), 'perCustomerUsageLimit'), + $limit, + ); + } + + /** + * @Then this coupon should not be reusable from cancelled orders + */ + public function thisCouponShouldNotBeReusableFromCancelledOrders(): void + { + Assert::false($this->responseChecker->getValue( + $this->client->getLastResponse(), + 'reusableFromCancelledOrders', + )); + } + + /** + * @Then I should be notified that it has been successfully deleted + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyDeleted(): void + { + Assert::true( + $this->responseChecker->isDeletionSuccessful($this->client->getLastResponse()), + 'Promotion coupon could not be deleted', + ); + } + + /** + * @Then /^(this coupon) should no longer exist in the coupon registry$/ + */ + public function couponShouldNotExistInTheRegistry(PromotionCouponInterface $coupon): void + { + Assert::false($this->responseChecker->hasItemWithValue( + $this->client->index(Resources::PROMOTION_COUPONS), + 'code', + $coupon->getCode(), + )); + } + + /** + * @Then /^(this coupon) should still exist in the registry$/ + */ + public function couponShouldStillExistInTheRegistry(PromotionCouponInterface $coupon): void + { + Assert::true($this->responseChecker->hasItemWithValue( + $this->client->index(Resources::PROMOTION_COUPONS), + 'code', + $coupon->getCode(), + )); + } + + /** + * @Then all of the coupon codes should be prefixed with :prefix + */ + public function allOfTheCouponCodesShouldBePrefixedWith(string $prefix): void + { + foreach ($this->responseChecker->getCollection($this->client->getLastResponse()) as $promotionCoupon) { + Assert::startsWith($promotionCoupon['code'], $prefix); + } + } + + /** + * @Then all of the coupon codes should be suffixed with :suffix + */ + public function allOfTheCouponCodesShouldBeSuffixedWith(string $suffix): void + { + foreach ($this->responseChecker->getCollection($this->client->getLastResponse()) as $promotionCoupon) { + Assert::endsWith($promotionCoupon['code'], $suffix); + } + } + + /** + * @Then /^there should still be only one coupon with code "([^"]+)" related to (this promotion)$/ + */ + public function thereShouldStillBeOnlyOneCouponWithCodeRelatedTo(string $code, PromotionInterface $promotion): void + { + $coupons = $this->responseChecker->getCollectionItemsWithValue( + $this->client->subResourceIndex(Resources::PROMOTIONS, Subresources::PROMOTION_COUPONS, $promotion->getCode()), + 'code', + $code, + ); + + Assert::count($coupons, 1); + } + + /** + * @Then I should be notified that it is in use and cannot be deleted + */ + public function iShouldBeNotifiedThatItIsInUseAndCannotBeDeleted(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Cannot delete, the promotion coupon is in use.', + ); + } + + /** + * @Then I should be notified that code is required + */ + public function iShouldBeNotifiedThatCodeIsRequired(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same($this->responseChecker->getError($response), 'code: Please enter coupon code.'); + } + + /** + * @Then I should be notified that coupon usage limit must be at least one + */ + public function iShouldBeNotifiedThatCouponUsageLimitMustBeAtLeastOne(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'usageLimit: Coupon usage limit must be at least 1.', + ); + } + + /** + * @Then I should be notified that coupon usage limit per customer must be at least one + */ + public function iShouldBeNotifiedThatCouponUsageLimitPerCustomerMustBeAtLeastOne(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'perCustomerUsageLimit: Coupon usage limit per customer must be at least 1.', + ); + } + + /** + * @Then I should be notified that promotion is required + */ + public function iShouldBeNotifiedThatPromotionIsRequired(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'promotion: Please provide a promotion for this coupon.', + ); + } + + /** + * @Then I should be notified that only coupon based promotions can have coupons + */ + public function iShouldBeNotifiedThatOnlyCouponBasedPromotionsCanHaveCoupons(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'promotion: Only coupon based promotions can have coupons.', + ); + } + + /** + * @Then I should be notified that generating :amount coupons with code length equal to :codeLength is not possible + */ + public function iShouldBeNotifiedThatGeneratingCouponsWithCodeLengthIsNotPossible(int $amount, int $codeLength): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf( + 'Invalid coupon code length or coupons amount. It is not possible to generate %d unique coupons with %d code length', + $amount, + $codeLength, + ), + ); + } + + /** + * @Then I should be notified that generate amount is required + */ + public function iShouldBeNotifiedThatGenerateAmountIsRequired(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'amount: Please enter amount of coupons to generate.', + ); + } + + /** + * @Then I should be notified that generate code length is required + */ + public function iShouldBeNotifiedThatCodeLengthIsRequired(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'codeLength: Please enter coupon code length.', + ); + } + + /** + * @Then I should be notified that generate code length is out of range + */ + public function iShouldBeNotifiedThatCodeLengthIsOutOfRange(): void + { + Assert::same( + $this->responseChecker->getError($this->client->getLastResponse()), + 'codeLength: Coupon code length must be between 1 and 40.', + ); + } + + /** + * @Then I should be notified that they have been successfully generated + */ + public function iShouldBeNotifiedThatTheyHaveBeenSuccessfullyGenerated(): void + { + Assert::true( + $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), + 'Promotion coupon could not be generated', + ); + } + + /** + * @Then I should be notified that coupon with this code already exists + */ + public function iShouldBeNotifiedThatCouponWithThisCodeAlreadyExists(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Coupon has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'code: This coupon already exists.', + ); + } + + /** + * @Then I should not be able to edit its code + */ + public function iShouldNotBeAbleToEditItsCode(): void + { + $this->client->updateRequestData(['code' => 'NEW_CODE']); + + Assert::false($this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE')); + } + + /** + * @Then /^("[^"]+" coupon) should be used (\d+) time(?:|s)$/ + */ + public function couponShouldHaveUsageLimit(PromotionCouponInterface $promotionCoupon, int $used): void + { + $returnedPromotionCoupon = current($this->responseChecker->getCollectionItemsWithValue( + $this->client->getLastResponse(), + 'code', + $promotionCoupon->getCode(), + )); + + Assert::same( + $returnedPromotionCoupon['used'], + $used, + sprintf('The promotion coupon %s has been used %s times', $promotionCoupon->getCode(), $returnedPromotionCoupon['used']), + ); + } + + private function sortBy(string $order, string $field): void + { + $this->client->sort([$field => str_starts_with($order, 'de') ? 'desc' : 'asc']); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php index 23cbcfb802..722219236b 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingPromotionsContext.php @@ -13,11 +13,28 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; +use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\PromotionInterface; +use Sylius\Component\Core\Model\TaxonInterface; +use Sylius\Component\Core\Promotion\Action\FixedDiscountPromotionActionCommand; +use Sylius\Component\Core\Promotion\Action\PercentageDiscountPromotionActionCommand; +use Sylius\Component\Core\Promotion\Action\ShippingPercentageDiscountPromotionActionCommand; +use Sylius\Component\Core\Promotion\Action\UnitFixedDiscountPromotionActionCommand; +use Sylius\Component\Core\Promotion\Action\UnitPercentageDiscountPromotionActionCommand; +use Sylius\Component\Core\Promotion\Checker\Rule\ContainsProductRuleChecker; +use Sylius\Component\Core\Promotion\Checker\Rule\CustomerGroupRuleChecker; +use Sylius\Component\Core\Promotion\Checker\Rule\HasTaxonRuleChecker; +use Sylius\Component\Core\Promotion\Checker\Rule\TotalOfItemsFromTaxonRuleChecker; +use Sylius\Component\Customer\Model\CustomerGroupInterface; +use Sylius\Component\Promotion\Checker\Rule\ItemTotalRuleChecker; +use Symfony\Component\HttpFoundation\Request; use Webmozart\Assert\Assert; final class ManagingPromotionsContext implements Context @@ -25,6 +42,8 @@ final class ManagingPromotionsContext implements Context public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, + private IriConverterInterface $iriConverter, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, ) { } @@ -45,13 +64,43 @@ final class ManagingPromotionsContext implements Context $this->client->buildCreateRequest(Resources::PROMOTIONS); } + /** + * @When I want to modify a :promotion promotion + * @When /^I want to modify (this promotion)$/ + * @When I modify a :promotion promotion + */ + public function iWantToModifyAPromotion(PromotionInterface $promotion): void + { + $this->client->buildUpdateRequest(Resources::PROMOTIONS, $promotion->getCode()); + } + + /** + * @When I archive the :promotion promotion + */ + public function iArchiveThePromotion(PromotionInterface $promotion): void + { + $this->client->customItemAction(Resources::PROMOTIONS, $promotion->getCode(), Request::METHOD_PATCH, 'archive'); + $this->client->index(Resources::PROMOTIONS); + } + + /** + * @When I restore the :promotion promotion + */ + public function iRestoreThePromotion(PromotionInterface $promotion): void + { + $this->client->customItemAction(Resources::PROMOTIONS, $promotion->getCode(), Request::METHOD_PATCH, 'restore'); + } + /** * @When I specify its :field as :value + * @When I do not specify its :field * @When I :field it :value */ - public function iSpecifyItsAs(string $field, string $value): void + public function iSpecifyItsAs(string $field, ?string $value = null): void { - $this->client->addRequestData($field, $value); + if (null !== $value) { + $this->client->addRequestData($field, $value); + } } /** @@ -62,8 +111,335 @@ final class ManagingPromotionsContext implements Context $this->client->updateRequestData(['appliesToDiscounted' => false]); } + /** + * @When I set its usage limit to :usageLimit + */ + public function iSetItsUsageLimitTo(int $usageLimit): void + { + $this->client->addRequestData('usageLimit', $usageLimit); + } + + /** + * @When I set it as exclusive + */ + public function iSetItAsExclusive(): void + { + $this->client->addRequestData('exclusive', true); + } + + /** + * @When I make it coupon based + */ + public function iMakeItCouponBased(): void + { + $this->client->addRequestData('couponBased', true); + } + + /** + * @When I set its priority to :priority + * @When I remove its priority + */ + public function iRemoveItsPriority(?int $priority = null): void + { + $this->client->addRequestData('priority', $priority); + } + + /** + * @When I do not name it + * @When I remove its name + */ + public function iNameIt(string $name = ''): void + { + $this->client->addRequestData('name', $name); + } + + /** + * @When I make it applicable for the :channel channel + */ + public function iMakeItApplicableForTheChannel(ChannelInterface $channel): void + { + $this->client->addRequestData('channels', [$this->iriConverter->getIriFromItem($channel)]); + } + + /** + * @When I make it available from :startsDate to :endsDate + */ + public function iMakeItAvailableFromTo(\DateTimeInterface $startsDate, \DateTimeInterface $endsDate): void + { + $this->client->updateRequestData([ + 'startsAt' => $startsDate->format('Y-m-d H:i:s'), + 'endsAt' => $endsDate->format('Y-m-d H:i:s'), + ]); + } + + /** + * @When I specify its label as :label in :localeCode locale + */ + public function iSpecifyItsLabelInLocaleCode(string $label, string $localeCode): void + { + $data['translations'][$localeCode]['label'] = $label; + + $this->client->updateRequestData($data); + } + + /** + * @When I replace its label with a string exceeding the limit in :localeCode locale + */ + public function iSpecifyItsLabelWithAStringExceedingTheLimitInLocale(string $localeCode): void + { + $this->iSpecifyItsLabelInLocaleCode(str_repeat('a', 256), $localeCode); + } + + /** + * @When /^I add the "([^"]+)" action configured with amount of "(?:€|£|\$)([^"]+)" for ("[^"]+" channel)$/ + */ + public function iAddTheActionConfiguredWithAmountForChannel( + string $actionType, + int $amount, + ChannelInterface $channel, + ): void { + $actionTypeMapping = [ + 'Order fixed discount' => FixedDiscountPromotionActionCommand::TYPE, + 'Item fixed discount' => UnitFixedDiscountPromotionActionCommand::TYPE, + ]; + + $this->addToRequestAction( + $actionTypeMapping[$actionType], + [ + $channel->getCode() => [ + 'amount' => $amount, + ], + ], + ); + } + + /** + * @When /^I add the "Item percentage discount" action configured with a percentage value of ("[^"]+") for ("[^"]+" channel)$/ + */ + public function iAddTheActionConfiguredWithAPercentageValueForChannel( + float $percentage, + ChannelInterface $channel, + ): void { + $this->addToRequestAction( + UnitPercentageDiscountPromotionActionCommand::TYPE, + [ + $channel->getCode() => [ + 'percentage' => $percentage, + ], + ], + ); + } + + /** + * @When I add the "Item percentage discount" action configured without a percentage value for :channel channel + */ + public function iAddTheActionConfiguredWithoutAPercentageValueForChannel(ChannelInterface $channel): void + { + $this->addToRequestAction( + UnitPercentageDiscountPromotionActionCommand::TYPE, + [ + $channel->getCode() => [ + 'percentage' => null, + ], + ], + ); + } + + /** + * @When /^I add the "([^"]+)" action configured with a percentage value of ("[^"]+")$/ + * @When I add the :actionType action configured without a percentage value + */ + public function iAddTheActionConfiguredWithAPercentageValue(string $actionType, ?float $percentage = null): void + { + $actionTypeMapping = [ + 'Order percentage discount' => PercentageDiscountPromotionActionCommand::TYPE, + 'Shipping percentage discount' => ShippingPercentageDiscountPromotionActionCommand::TYPE, + ]; + + $this->addToRequestAction( + $actionTypeMapping[$actionType], + [ + 'percentage' => $percentage, + ], + ); + } + + /** + * @When /^it is(?:| also) configured with amount of "(?:€|£|\$)([^"]+)" for ("[^"]+" channel)$/ + */ + public function itIsConfiguredWithAmountForChannel(float $amount, ChannelInterface $channel): void + { + $actions = $this->getActions(); + $actions[0]['configuration'][$channel->getCode()]['amount'] = $amount; + + $this->client->addRequestData('actions', $actions); + } + + /** + * @When /^I edit (this promotion) percentage action to have ("[^"]+")$/ + */ + public function iEditPromotionToHaveDiscount(PromotionInterface $promotion, float $percentage): void + { + $actions = $this->getActions(); + $actions[0]['configuration']['percentage'] = $percentage; + + $this->client->addRequestData('actions', $actions); + } + + /** + * @When /^I specify that on ("[^"]+" channel) this action should be applied to items with price greater than "(?:€|£|\$)([^"]+)"$/ + */ + public function iAddAMinPriceFilterRangeForChannel(ChannelInterface $channel, int|string $minimum): void + { + $actions = $this->getActions(); + $actions[0]['configuration'][$channel->getCode()]['filters']['price_range_filter']['min'] = $minimum; + + $this->client->addRequestData('actions', $actions); + } + + /** + * @When /^I specify that on ("[^"]+" channel) this action should be applied to items with price lesser than "(?:€|£|\$)([^"]+)"$/ + */ + public function iAddAMaxPriceFilterRangeForChannel(ChannelInterface $channel, int|string $maximum): void + { + $actions = $this->getActions(); + $actions[0]['configuration'][$channel->getCode()]['filters']['price_range_filter']['max'] = $maximum; + + $this->client->addRequestData('actions', $actions); + } + + /** + * @When /^I specify that on ("[^"]+" channel) this action should be applied to items with price between "(?:€|£|\$)([^"]+)" and "(?:€|£|\$)([^"]+)"$/ + */ + public function iAddAMinMaxPriceFilterRangeForChannel(ChannelInterface $channel, int $minimum, int $maximum): void + { + $this->iAddAMinPriceFilterRangeForChannel($channel, $minimum); + $this->iAddAMaxPriceFilterRangeForChannel($channel, $maximum); + } + + /** + * @When I specify that this action should be applied to items from :taxon category + */ + public function iSpecifyThatThisActionShouldBeAppliedToItemsFromCategory(TaxonInterface $taxon): void + { + $actions = $this->getActions(); + $channelCode = key($actions[0]['configuration']); + $actions[0]['configuration'][$channelCode]['filters']['taxons_filter']['taxons'] = [$taxon->getCode()]; + + $this->client->addRequestData('actions', $actions); + } + + /** + * @When I specify that this action should be applied to the :product product + */ + public function iSpecifyThatThisActionShouldBeAppliedToTheProduct(ProductInterface $product): void + { + $actions = $this->getActions(); + $channelCode = key($actions[0]['configuration']); + $actions[0]['configuration'][$channelCode]['filters']['products_filter']['products'] = [$product->getCode()]; + + $this->client->addRequestData('actions', $actions); + } + + /** + * @When /^I add the "Has at least one from taxons" rule configured with ("[^"]+" taxon)$/ + * @When /^I add the "Has at least one from taxons" rule configured with ("[^"]+" taxon) and ("[^"]+" taxon)$/ + */ + public function iAddTheHasTaxonRuleConfiguredWith(TaxonInterface ...$taxons): void + { + $this->addToRequestRule( + HasTaxonRuleChecker::TYPE, + [ + 'taxons' => array_map(fn (TaxonInterface $taxon): string => $taxon->getCode(), $taxons), + ], + ); + } + + /** + * @When /^I add the "Total price of items from taxon" rule configured with ("[^"]+" taxon) and ("[^"]+") amount for ("[^"]+" channel)$/ + */ + public function iAddTheRuleConfiguredWith(TaxonInterface $taxon, int $amount, ChannelInterface $channel): void + { + $this->addToRequestRule( + TotalOfItemsFromTaxonRuleChecker::TYPE, + [ + $channel->getCode() => [ + 'amount' => $amount, + 'taxon' => $taxon->getCode(), + ], + ], + ); + } + + /** + * @When /^I add the "Item total" rule configured with ("[^"]+") amount for ("[^"]+" channel) and ("[^"]+") amount for ("[^"]+" channel)$/ + */ + public function iAddTheItemTotalRuleConfiguredWithTwoChannel( + int $firstAmount, + ChannelInterface $firstChannel, + int $secondAmount, + ChannelInterface $secondChannel, + ): void { + $this->addToRequestRule( + ItemTotalRuleChecker::TYPE, + [ + $firstChannel->getCode() => [ + 'amount' => $firstAmount, + ], + $secondChannel->getCode() => [ + 'amount' => $secondAmount, + ], + ], + ); + } + + /** + * @When I add the "Contains product" rule configured with the :product product + */ + public function iAddTheRuleConfiguredWithTheProduct(ProductInterface $product): void + { + $this->addToRequestRule( + ContainsProductRuleChecker::TYPE, + [ + 'product_code' => $product->getCode(), + ], + ); + } + + /** + * @When I add the "Customer group" rule for :customerGroup group + */ + public function iAddTheCustomerGroupRuleConfiguredForGroup(CustomerGroupInterface $customerGroup): void + { + $this->addToRequestRule( + CustomerGroupRuleChecker::TYPE, + [ + 'group_code' => $customerGroup->getCode(), + ], + ); + } + + /** + * @When I filter promotions by coupon code equal :value + */ + public function iFilterPromotionsByCouponCodeEqual(string $value): void + { + $this->client->addFilter('coupons.code', $value); + $this->client->filter(); + } + + /** + * @When I filter archival promotions + */ + public function iFilterArchivalPromotions(): void + { + $this->client->addFilter('exists[archivedAt]', true); + $this->client->filter(); + } + /** * @When I add it + * @When I try to add it */ public function iAddIt(): void { @@ -83,17 +459,39 @@ final class ManagingPromotionsContext implements Context } /** + * @Then the :promotionName promotion should appear in the registry * @Then the :promotionName promotion should exist in the registry + * @Then promotion :promotionName should still exist in the registry + * @Then this promotion should still be named :promotionName */ public function thePromotionShouldAppearInTheRegistry(string $promotionName): void { - $returnedPromotion = current($this->responseChecker->getCollectionItemsWithValue( - $this->client->getLastResponse(), - 'name', - $promotionName, - )); + Assert::true( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::PROMOTIONS), 'name', $promotionName), + sprintf('Promotion with name %s does not exist', $promotionName), + ); + } - Assert::notNull($returnedPromotion, sprintf('There is no promotion %s in registry', $promotionName)); + /** + * @Then I should see the promotion :promotionName in the list + */ + public function iShouldSeeThePromotionInTheList(string $promotionName): void + { + Assert::true( + $this->responseChecker->hasItemWithValue($this->client->getLastResponse(), 'name', $promotionName), + sprintf('Promotion with name %s does not exist', $promotionName), + ); + } + + /** + * @Then I should not see the promotion :promotionName in the list + */ + public function iShouldNotSeeThePromotionInTheList(string $promotionName): void + { + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->getLastResponse(), 'name', $promotionName), + sprintf('Promotion with name %s does not exist', $promotionName), + ); } /** @@ -178,4 +576,345 @@ final class ManagingPromotionsContext implements Context $this->responseChecker->getValue($this->client->show(Resources::PROMOTIONS, $promotion->getCode()), 'appliesToDiscounted'), ); } + + /** + * @Then the :promotion promotion should be available to be used only :usageLimit times + */ + public function thePromotionShouldBeAvailableToUseOnlyTimes(PromotionInterface $promotion, int $usageLimit): void + { + Assert::true( + $this->responseChecker->hasValue( + $this->client->show(Resources::PROMOTIONS, $promotion->getCode()), + 'usageLimit', + $usageLimit, + ), + ); + } + + /** + * @Then the :promotion promotion should be exclusive + */ + public function thePromotionShouldBeExclusive(PromotionInterface $promotion): void + { + Assert::true( + $this->responseChecker->getValue( + $this->client->show(Resources::PROMOTIONS, $promotion->getCode()), + 'exclusive', + ), + ); + } + + /** + * @Then the :promotion promotion should be coupon based + */ + public function thePromotionShouldBeCouponBased(PromotionInterface $promotion): void + { + Assert::true( + $this->responseChecker->getValue( + $this->client->show(Resources::PROMOTIONS, $promotion->getCode()), + 'couponBased', + ), + ); + } + + /** + * @Then the :promotion promotion should be applicable for the :channel channel + */ + public function thePromotionShouldBeApplicableForTheChannel(PromotionInterface $promotion, ChannelInterface $channel): void + { + Assert::true( + $this->responseChecker->hasValueInCollection( + $this->client->show(Resources::PROMOTIONS, $promotion->getCode()), + 'channels', + $this->sectionAwareIriConverter->getIriFromResourceInSection($channel, 'admin'), + ), + ); + } + + /** + * @When the :promotion promotion should have a label :label in :localeCode locale + */ + public function thePromotionShouldHaveLabelInLocale(PromotionInterface $promotion, string $label, string $localeCode): void + { + $response = $this->client->show(Resources::PROMOTIONS, $promotion->getCode()); + + Assert::true($this->responseChecker->hasTranslation($response, $localeCode, 'label', $label)); + } + + /** + * @Then /^it should have ("[^"]+") of item percentage discount configured for ("[^"]+" channel)$/ + */ + public function itShouldHaveOfItemPercentageDiscount(float $percentage, ChannelInterface $channel): void + { + $actions = $this->responseChecker->getValue($this->client->getLastResponse(), 'actions'); + foreach ($actions as $action) { + if ($action['type'] === 'unit_percentage_discount') { + Assert::same((float) $action['configuration'][$channel->getCode()]['percentage'], $percentage); + } + } + } + + /** + * @Then /^it should have ("[^"]+") of order percentage discount$/ + */ + public function itShouldHaveOfOrderPercentageDiscount(float $percentage): void + { + $actions = $this->responseChecker->getValue($this->client->getLastResponse(), 'actions'); + Assert::same((float) $actions[0]['configuration']['percentage'], $percentage); + } + + /** + * @Then I should not be able to edit its code + */ + public function iShouldNotBeAbleToEditItsCode(): void + { + $this->client->updateRequestData(['code' => 'NEW_CODE']); + + Assert::false($this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE')); + } + + /** + * @Then the :promotion promotion should be available from :startsDate to :endsDate + */ + public function thePromotionShouldBeAvailableFromTo( + PromotionInterface $promotion, + \DateTimeInterface $startsDate, + \DateTimeInterface $endsDate, + ): void { + Assert::true( + $this->responseChecker->hasItemWithValues( + $this->client->index(Resources::PROMOTIONS), + [ + 'name' => $promotion->getName(), + 'startsAt' => $startsDate->format('Y-m-d H:i:s'), + 'endsAt' => $endsDate->format('Y-m-d H:i:s'), + ], + ), + ); + } + + /** + * @Then I should be able to modify a :promotion promotion + */ + public function iShouldBeAbleToModifyAPromotion(PromotionInterface $promotion): void + { + $this->iWantToModifyAPromotion($promotion); + $this->client->updateRequestData(['name' => 'NEW_NAME']); + + Assert::true($this->responseChecker->hasValue($this->client->update(), 'name', 'NEW_NAME')); + } + + /** + * @Then the :promotion promotion should have priority :priority + */ + public function thePromotionsShouldHavePriority(PromotionInterface $promotion, int $priority): void + { + Assert::true( + $this->responseChecker->hasItemWithValues( + $this->client->index(Resources::PROMOTIONS), + [ + 'name' => $promotion->getName(), + 'priority' => $priority, + ], + ), + ); + } + + /** + * @Then I should be notified that it is in use and cannot be deleted + */ + public function iShouldBeNotifiedThatItIsIUseAndCannotBeDeleted(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Cannot delete, the promotion is in use.', + ); + } + + /** + * @Then I should be notified that promotion with this code already exists + */ + public function iShouldBeNotifiedThatPromotionWithThisCodeAlreadyExists(): void + { + $response = $this->client->getLastResponse(); + Assert::false($this->responseChecker->isCreationSuccessful($response)); + Assert::same( + $this->responseChecker->getError($response), + 'code: The promotion with given code already exists.', + ); + } + + /** + * @Then there should still be only one promotion with :element :value + */ + public function thereShouldStillBeOnlyOnePromotionWith(string $element, string $value): void + { + Assert::count( + $this->responseChecker->getCollectionItemsWithValue($this->client->index(Resources::PROMOTIONS), $element, $value), + 1, + ); + } + + /** + * @Then promotion with :element :value should not be added + */ + public function promotionWithElementValueShouldNotBeAdded(string $element, string $value): void + { + Assert::false($this->responseChecker->hasItemWithValue($this->client->index(Resources::PROMOTIONS), $element, $value)); + } + + /** + * @Then I should be notified that :element is required + */ + public function iShouldBeNotifiedThatIsRequired(string $element): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('%s: Please enter promotion %s.', $element, $element), + ); + } + + /** + * @Then I should be notified that promotion cannot end before it starts + */ + public function iShouldBeNotifiedThatPromotionCannotEndBeforeItsEvenStarts(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'endsAt: End date cannot be set prior start date.', + ); + } + + /** + * @Then I should be notified that promotion label in :localeCode locale is too long + */ + public function iShouldBeNotifiedThatPromotionLabelIsTooLong(string $localeCode): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('translations[%s].label: This value is too long. It should have 255 characters or less.', $localeCode), + ); + } + + /** + * @Then I should be notified that this value should not be blank + */ + public function iShouldBeNotifiedThatThisValueShouldNotBeBlank(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'This value should not be blank.', + ); + } + + /** + * @Then I should be notified that a percentage discount value must be between 0% and 100% + * @Then I should be notified that a percentage discount value must be at least 0% + * @Then I should be notified that the maximum value of a percentage discount is 100% + */ + public function iShouldBeNotifiedThatPercentageDiscountShouldBeBetween(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'The percentage discount must be between 0% and 100%.', + ); + } + + /** + * @Then I should be notified that a minimum value should be a numeric value + */ + public function iShouldBeNotifiedThatAMinimalValueShouldBeNumeric(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + '[min]: This value should be of type numeric.', + ); + } + + /** + * @Then I should be notified that a maximum value should be a numeric value + */ + public function iShouldBeNotifiedThatAMaximumValueShouldBeNumeric(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + '[max]: This value should be of type numeric.', + ); + } + + /** + * @Then I should see :count promotions on the list + * @Then I should see a single promotion on the list + */ + public function iShouldSeePromotionInTheList(int $count = 1): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $count); + } + + /** + * @Then /^the (first|last) promotion on the list should have ([^"]+) "([^"]+)"$/ + */ + public function theFirstPromotionOnTheListShouldHave(string $togglePosition, string $field, string $value): void + { + $items = $this->responseChecker->getValue($this->client->getLastResponse(), 'hydra:member'); + if ('first' === $togglePosition) { + $item = reset($items); + } else { + $item = end($items); + } + + Assert::same($item[$field], $value); + } + + /** + * @Then the promotion :promotion should be used :usage time(s) + * @Then the promotion :promotion should not be used + */ + public function thePromotionShouldBeUsedTime(PromotionInterface $promotion, int $usage = 0): void + { + $returnedPromotion = current($this->responseChecker->getCollectionItemsWithValue( + $this->client->getLastResponse(), + 'code', + $promotion->getCode(), + )); + + Assert::same( + $returnedPromotion['used'], + $usage, + sprintf('The promotion %s has been used %s times', $promotion->getName(), $returnedPromotion['used']), + ); + } + + /** + * @Then I should be viewing non archival promotions + */ + public function iShouldBeViewingNonArchivalPromotions(): void + { + $this->client->index(Resources::PROMOTIONS); + } + + private function addToRequestAction(string $type, array $configuration): void + { + $data['actions'][] = [ + 'type' => $type, + 'configuration' => $configuration, + ]; + + $this->client->updateRequestData($data); + } + + private function getActions(): array + { + return $this->client->getContent()['actions']; + } + + private function addToRequestRule(string $type, array $configuration): void + { + $data['rules'][] = [ + 'type' => $type, + 'configuration' => $configuration, + ]; + + $this->client->updateRequestData($data); + } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingShipmentsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingShipmentsContext.php index 51e113c98c..1453b71407 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingShipmentsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingShipmentsContext.php @@ -13,11 +13,12 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Channel\Model\ChannelInterface; use Sylius\Component\Core\Formatter\StringInflector; @@ -36,6 +37,7 @@ final class ManagingShipmentsContext implements Context private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, private SharedStorageInterface $sharedStorage, private string $apiUrlPrefix, ) { @@ -57,6 +59,19 @@ final class ManagingShipmentsContext implements Context $this->client->addFilter('state', $state); } + /** + * @When I move to the details of first shipment's order + */ + public function iMoveToDetailsOfFirstShipment(): void + { + $firstShipment = $this->responseChecker->getCollection($this->client->getLastResponse())[0]; + + /** @var OrderInterface $order */ + $order = $this->iriConverter->getResourceFromIri($firstShipment['order']); + + $this->client->customItemAction(Resources::ORDERS, $order->getTokenValue(), HttpRequest::METHOD_GET, 'shipments'); + } + /** * @When I choose :channel as a channel filter */ @@ -140,7 +155,10 @@ final class ManagingShipmentsContext implements Context */ public function iShouldBeNotifiedThatTheShipmentHasBeenSuccessfullyShipped(): void { - Assert::same($this->responseChecker->getValue($this->client->getLastResponse(), 'state'), 'shipped', 'Shipment is not shipped'); + Assert::true( + $this->responseChecker->isAccepted($this->client->getLastResponse()), + 'Shipment was not successfully shipped', + ); } /** @@ -162,7 +180,7 @@ final class ManagingShipmentsContext implements Context { Assert::true( $this->responseChecker->hasItemWithValues($this->client->index(Resources::SHIPMENTS), [ - 'order' => $this->iriConverter->getIriFromItemInSection($order, 'admin'), + 'order' => $this->sectionAwareIriConverter->getIriFromResourceInSection($order, 'admin'), 'state' => strtolower($shippingState), ]), sprintf('Shipment for order %s with state %s does not exist', $order->getNumber(), $shippingState), @@ -179,7 +197,7 @@ final class ManagingShipmentsContext implements Context $this->client->getLastResponse(), --$position, 'order', - $this->iriConverter->getIriFromItem($order), + $this->iriConverter->getIriFromResource($order), ), sprintf('On position %s there is no shipment for order %s', $position, $order->getNumber()), ); @@ -288,12 +306,27 @@ final class ManagingShipmentsContext implements Context Assert::same($productUnitsCounter, $amount); } + /** + * @Then I should see the details of order :order + */ + public function iShouldSeeOrderWithDetails(OrderInterface $order): void + { + Assert::true( + $this->responseChecker->hasItemWithValue( + $this->client->getLastResponse(), + 'order', + $this->sectionAwareIriConverter->getIriFromResourceInSection($order, 'admin'), + ), + sprintf('Order with number %s does not exist', $order->getNumber()), + ); + } + private function isShipmentForOrder(OrderInterface $order): bool { return $this->responseChecker->hasItemWithValue( $this->client->getLastResponse(), 'order', - $this->iriConverter->getIriFromItemInSection($order, 'admin'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($order, 'admin'), ); } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingShippingCategoriesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingShippingCategoriesContext.php index 81ed4dc432..ab72f7f500 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingShippingCategoriesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingShippingCategoriesContext.php @@ -107,14 +107,6 @@ final class ManagingShippingCategoriesContext implements Context $this->client->addRequestData('description', $description); } - /** - * @When I save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @Then I should be notified that shipping category with this code already exists */ @@ -228,17 +220,6 @@ final class ManagingShippingCategoriesContext implements Context ); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Shipping category could not be edited', - ); - } - /** * @Then I should be notified that it has been successfully deleted */ diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingShippingMethodsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingShippingMethodsContext.php index f3466dae5d..1815e36492 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingShippingMethodsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingShippingMethodsContext.php @@ -13,16 +13,19 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Addressing\Model\ZoneInterface; +use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ShippingMethodInterface; +use Sylius\Component\Shipping\Calculator\DefaultCalculators; use Symfony\Component\HttpFoundation\Request as HttpRequest; use Webmozart\Assert\Assert; @@ -34,6 +37,7 @@ final class ManagingShippingMethodsContext implements Context private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private IriConverterInterface $iriConverter, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, private SharedStorageInterface $sharedStorage, ) { } @@ -61,6 +65,82 @@ final class ManagingShippingMethodsContext implements Context ]); } + /** + * @When I add the :rule rule configured with :weight + */ + public function iAddTheRuleConfiguredWithWeight(string $rule, int $weight): void + { + $type = StringInflector::nameToLowercaseCode($rule); + $this->client->addRequestData('rules', [[ + 'type' => $type, + 'configuration' => [ + 'weight' => $weight, + ], + ]]); + } + + /** + * @When I add the "Total weight greater than or equal" rule configured with invalid data + */ + public function iAddTheTotalWeightGreaterThanOrEqualRuleConfiguredWithInvalidData(): void + { + $this->client->addRequestData('rules', [ + [ + 'type' => 'total_weight_greater_than_or_equal', + 'configuration' => [ + 'weight' => true, + ], + ], + ]); + } + + /** + * @When /^I add the "([^"]+)" rule configured with (?:€|£|\$)([^"]+) for ("[^"]+" channel)$/ + */ + public function iAddTheRuleConfiguredWithForChannel(string $rule, int $value, ChannelInterface $channel): void + { + match ($rule) { + 'Items total less than or equal' => $this->client->addRequestData('rules', [ + [ + 'type' => 'order_total_less_than_or_equal', + 'configuration' => [ + $channel->getCode() => [ + 'amount' => $value, + ], + ], + ], + ]), + 'Items total greater than or equal' => $this->client->addRequestData('rules', [ + [ + 'type' => 'order_total_greater_than_or_equal', + 'configuration' => [ + $channel->getCode() => [ + 'amount' => $value, + ], + ], + ], + ]), + default => throw new \InvalidArgumentException('Unsupported shipping method rule'), + }; + } + + /** + * @When /^I add the "Items total less than or equal" rule configured with invalid data for ("[^"]+" channel)$/ + */ + public function iAddTheItemsTotalLessThanOrEqualRuleConfiguredWithInvalidData(ChannelInterface $channel): void + { + $this->client->addRequestData('rules', [ + [ + 'type' => 'order_total_greater_than_or_equal', + 'configuration' => [ + $channel->getCode() => [ + 'amount' => true, + ], + ], + ], + ]); + } + /** * @When I change my locale to :localeCode */ @@ -114,13 +194,36 @@ final class ManagingShippingMethodsContext implements Context $this->client->setRequestData([ 'code' => 'FED_EX_CARRIER', 'position' => 0, - 'translations' => ['en_US' => ['name' => 'FedEx Carrier', 'locale' => 'en_US']], - 'zone' => $this->iriConverter->getIriFromItem($this->sharedStorage->get('zone')), + 'translations' => ['en_US' => ['name' => 'FedEx Carrier']], + 'zone' => $this->iriConverter->getIriFromResource($this->sharedStorage->get('zone')), 'calculator' => 'Flat rate per shipment', 'configuration' => [$this->sharedStorage->get('channel')->getCode() => ['amount' => 50]], ]); } + /** + * @When I do not specify amount for :calculatorName calculator + */ + public function iDoNotSpecifyAmountForCalculator(string $calculatorName): void + { + match ($calculatorName) { + 'Flat rate per shipment' => $this->client->addRequestData('calculator', DefaultCalculators::FLAT_RATE), + 'Flat rate per unit' => $this->client->addRequestData('calculator', DefaultCalculators::PER_UNIT_RATE), + 'default' => throw new \InvalidArgumentException('Unsupported calculator name'), + }; + + $channelCode = $this->sharedStorage->get('channel')->getCode(); + $this->client->addRequestData('configuration', [$channelCode => ['amount' => null]]); + } + + /** + * @When I remove its zone + */ + public function iRemoveItsZone(): void + { + $this->client->replaceRequestData('zone', null); + } + /** * @When I try to show :shippingMethod shipping method */ @@ -162,7 +265,7 @@ final class ManagingShippingMethodsContext implements Context */ public function iNameItIn(?string $name = '', ?string $localeCode = 'en_US'): void { - $this->client->updateRequestData(['translations' => [$localeCode => ['name' => $name, 'locale' => $localeCode]]]); + $this->client->updateRequestData(['translations' => [$localeCode => ['name' => $name]]]); } /** @@ -170,7 +273,7 @@ final class ManagingShippingMethodsContext implements Context */ public function iDescribeItAsIn(string $description, string $localeCode): void { - $data = ['translations' => [$localeCode => ['locale' => $localeCode]]]; + $data = ['translations' => [$localeCode => []]]; $data['translations'][$localeCode]['description'] = $description; $this->client->updateRequestData($data); @@ -183,7 +286,7 @@ final class ManagingShippingMethodsContext implements Context public function iDefineItForTheZone(ZoneInterface $zone = null): void { if (null !== $zone) { - $this->client->addRequestData('zone', $this->iriConverter->getIriFromItem($zone)); + $this->client->addRequestData('zone', $this->iriConverter->getIriFromResource($zone)); } } @@ -208,7 +311,7 @@ final class ManagingShippingMethodsContext implements Context */ public function iMakeItAvailableInChannel(ChannelInterface $channel): void { - $this->client->addRequestData('channels', [$this->iriConverter->getIriFromItem($channel)]); + $this->client->addRequestData('channels', [$this->iriConverter->getIriFromResource($channel)]); } /** @@ -254,14 +357,6 @@ final class ManagingShippingMethodsContext implements Context $this->client->buildUpdateRequest(Resources::SHIPPING_METHODS, $shippingMethod->getCode()); } - /** - * @When I (try to) save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @When I sort the shipping methods :sortType by code * @When I switch the way shipping methods are sorted :sortType by code @@ -316,6 +411,17 @@ final class ManagingShippingMethodsContext implements Context ); } + /** + * @Then the shipping method :name should not appear in the registry + */ + public function theShippingMethodShouldNotAppearInTheRegistry(string $name): void + { + Assert::false( + $this->responseChecker->hasItemWithTranslation($this->client->index(Resources::SHIPPING_METHODS), 'en_US', 'name', $name), + sprintf('Shipping method with name %s exists', $name), + ); + } + /** * @Then /^(this shipping method) should still be in the registry$/ */ @@ -381,7 +487,7 @@ final class ManagingShippingMethodsContext implements Context $this->responseChecker->hasValueInCollection( $this->client->show(Resources::SHIPPING_METHODS, $shippingMethod->getCode()), 'channels', - $this->iriConverter->getIriFromItemInSection($channel, 'admin'), + $this->sectionAwareIriConverter->getIriFromResourceInSection($channel, 'admin'), ), sprintf('Shipping method is not assigned to %s channel', $channel->getName()), ); @@ -447,17 +553,6 @@ final class ManagingShippingMethodsContext implements Context ); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Shipping method could not be edited', - ); - } - /** * @Then I should be notified that shipping method with this code already exists */ @@ -544,6 +639,17 @@ final class ManagingShippingMethodsContext implements Context ); } + /** + * @Then I should be notified that the zone is required + */ + public function iShouldBeNotifiedThatZoneHasToBeIriAndCannotBeNull(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Expected IRI or nested document for attribute "zone", "NULL" given.', + ); + } + /** * @Then shipping method with :element :value should not be added */ @@ -595,6 +701,72 @@ final class ManagingShippingMethodsContext implements Context // Intentionally left blank } + /** + * @Then shipping method :shippingMethod should still have code :code + */ + public function shippingMethodShouldStillHaveCode(ShippingMethodInterface $shippingMethod, string $code): void + { + Assert::same( + $this->responseChecker->getValue($this->client->show(Resources::SHIPPING_METHODS, $shippingMethod->getCode()), 'code'), + $code, + ); + } + + /** + * @Then I should be notified that amount for :channel channel should not be blank + */ + public function iShouldBeNotifiedThatAmountForChannelShouldNotBeBlank(ChannelInterface $channel): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('configuration[%s][amount]: This value should not be blank.', $channel->getCode()), + ); + } + + /** + * @Then I should be notified that code needs to contain only specific symbols + */ + public function iShouldBeNotifiedThatCodeNeedsToContainOnlySpecificSymbols(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'code: Shipping method code can only be comprised of letters, numbers, dashes and underscores.', + ); + } + + /** + * @Then I should be notified that shipping charge for :channel channel cannot be lower than 0 + */ + public function iShouldBeNotifiedThatShippingChargeForChannelCannotBeLowerThan0(ChannelInterface $channel): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('configuration[%s][amount]: Shipping charge cannot be lower than 0.', $channel->getCode()), + ); + } + + /** + * @Then I should be notified that the weight rule has an invalid configuration + */ + public function iShouldBeNotifiedThatTheWeightRuleHasAnInvalidConfiguration(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'configuration[weight]: This value should be of type numeric.', + ); + } + + /** + * @Then I should be notified that the amount rule has an invalid configuration in :channel channel + */ + public function iShouldBeNotifiedThatTheAmountRuleHasAnInvalidConfigurationInChannel(ChannelInterface $channel): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('configuration[%s][amount]: This value should be of type numeric.', $channel->getCode()), + ); + } + private function getAdminLocaleCode(): string { /** @var AdminUserInterface $adminUser */ diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxCategoriesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxCategoriesContext.php index 5ab759177d..fb6418d653 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxCategoriesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxCategoriesContext.php @@ -100,14 +100,6 @@ final class ManagingTaxCategoriesContext implements Context $this->client->addRequestData('description', $description); } - /** - * @When I (try to) save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @When I browse tax categories */ @@ -225,17 +217,6 @@ final class ManagingTaxCategoriesContext implements Context ); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Tax category could not be edited', - ); - } - /** * @Then I should be notified that it has been successfully deleted */ diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxRatesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxRatesContext.php new file mode 100644 index 0000000000..e34fab1da9 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxRatesContext.php @@ -0,0 +1,475 @@ +client->buildCreateRequest(Resources::TAX_RATES); + } + + /** + * @When I specify its code as :code + */ + public function iSpecifyItsCodeAs(string $code): void + { + $this->client->addRequestData('code', $code); + } + + /** + * @When I name it :name + * @When I rename it to :name + */ + public function iNameIt(string $name): void + { + $this->client->addRequestData('name', $name); + } + + /** + * @When I define it for the :zone zone + * @When I change its zone to :zone + */ + public function iDefineItForTheZone(ZoneInterface $zone): void + { + $this->client->addRequestData('zone', $this->iriConverter->getIriFromResource($zone)); + } + + /** + * @When I make it applicable for the :taxCategory tax category + * @When I change it to be applicable for the :taxCategory tax category + */ + public function iMakeItApplicableForTheTaxCategory(TaxCategoryInterface $taxCategory): void + { + $this->client->addRequestData('category', $this->iriConverter->getIriFromResource($taxCategory)); + } + + /** + * @When I specify its amount as :amount% + */ + public function iSpecifyItsAmountAs(string $amount): void + { + $this->client->addRequestData('amount', $amount); + } + + /** + * @When I do not specify related tax category + * @When I do not specify its zone + * @When I do not name it + * @When I do not specify its code + */ + public function iDoNotSpecifyItsField(): void + { + // Intentionally left blank + } + + /** + * @When I choose the default tax calculator + */ + public function iChooseTheDefaultTaxCalculator(): void + { + $this->client->addRequestData('calculator', 'default'); + } + + /** + * @When I make it start at :startDate and end at :endDate + */ + public function iMakeItStartAtAndEndAt(string $startDate, string $endDate): void + { + $this->client->addRequestData('startDate', $startDate); + $this->client->addRequestData('endDate', $endDate); + } + + /** + * @When I set the start date to :startDate + */ + public function iSetTheStartDateTo(string $startDate): void + { + $this->client->addRequestData('startDate', $startDate); + } + + /** + * @When I set the end date to :endDate + */ + public function iSetTheEndDateTo(string $endDate): void + { + $this->client->addRequestData('endDate', $endDate); + } + + /** + * @When I add it + * @When I try to add it + */ + public function iAddIt(): void + { + $this->client->create(); + } + + /** + * @When I choose "Included in price" option + */ + public function iChooseOption() + { + $this->client->addRequestData('includedInPrice', true); + } + + /** + * @When /^I want to modify (this tax rate)$/ + * @When I want to modify a tax rate :taxRate + */ + public function iWantToModifyThisTaxRate(TaxRateInterface $taxRate): void + { + $this->client->buildUpdateRequest(Resources::TAX_RATES, (string) $taxRate->getCode()); + $this->client->addRequestData('amount', (string) $taxRate->getAmount()); + } + + /** + * @When I browse tax rates + */ + public function iBrowseTaxRates(): void + { + $this->client->index(Resources::TAX_RATES); + } + + /** + * @When I remove its name + */ + public function iRemoveItsName(): void + { + $this->client->addRequestData('name', ''); + } + + /** + * @When I filter tax rates by start date from :startDate + */ + public function iFilterTaxRatesByStartDateFrom(string $startDate): void + { + $this->client->addFilter('startDate[after]', $startDate); + $this->client->filter(); + } + + /** + * @When I filter tax rates by start date up to :startDate + */ + public function iFilterTaxRatesByStartDateUpTo(string $startDate): void + { + $this->client->addFilter('startDate[before]', $startDate); + $this->client->filter(); + } + + /** + * @When I filter tax rates by start date from :startDate up to :endDate + */ + public function iFilterTaxRatesByStartDateFromUpTo(string $startDate, string $endDate): void + { + $this->client->addFilter('startDate[after]', $startDate); + $this->client->addFilter('startDate[before]', $endDate); + $this->client->filter(); + } + + /** + * @When I filter tax rates by end date from :endDate + */ + public function iFilterTaxRatesByEndDateFrom(string $endDate): void + { + $this->client->addFilter('endDate[after]', $endDate); + $this->client->filter(); + } + + /** + * @When I filter tax rates by end date up to :endDate + */ + public function iFilterTaxRatesByEndDateUpTo(string $endDate): void + { + $this->client->addFilter('endDate[before]', $endDate); + $this->client->filter(); + } + + /** + * @When I filter tax rates by end date from :startDate up to :endDate + */ + public function iFilterTaxRatesByEndDateFromUpTo(string $startDate, string $endDate): void + { + $this->client->addFilter('endDate[after]', $startDate); + $this->client->addFilter('endDate[before]', $endDate); + $this->client->filter(); + } + + /** + * @When I delete tax rate :taxRate + */ + public function iDeleteTaxRate(TaxRateInterface $taxRate): void + { + $this->client->delete(Resources::TAX_RATES, (string) $taxRate->getCode()); + } + + /** + * @Then I should be notified that it has been successfully created + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyCreated(): void + { + Assert::true( + $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), + 'Tax tax rate could not be created', + ); + } + + /** + * @Then the tax rate :taxRate should appear in the registry + * @Then I should see the tax rate :taxRate in the list + */ + public function theTaxRateShouldAppearInTheRegistry(TaxRateInterface $taxRate): void + { + $this->sharedStorage->set('tax_rate', $taxRate); + + $name = $taxRate->getName(); + + Assert::true( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::TAX_RATES), 'name', $name), + sprintf('Tax rate with name %s does not exist', $name), + ); + } + + /** + * @Then the tax rate :taxRate should be included in price + */ + public function theTaxRateShouldIncludePrice(TaxRateInterface $taxRate): void + { + Assert::true( + $taxRate->isIncludedInPrice(), + sprintf('Tax rate is not included in price'), + ); + } + + /** + * @Then I should be notified that it has been successfully deleted + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyDeleted(): void + { + Assert::true( + $this->responseChecker->isDeletionSuccessful($this->client->getLastResponse()), + 'Tax rate could not be deleted', + ); + } + + /** + * @Then /^(this tax rate) should no longer exist in the registry$/ + */ + public function thisTaxRateShouldNoLongerExistInTheRegistry(TaxRateInterface $taxRate): void + { + $name = $taxRate->getName(); + + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::TAX_RATES), 'name', $name), + sprintf('Tax rate with name %s exists', $name), + ); + } + + /** + * @Then I should see a single tax rate in the list + */ + public function iShouldSeeASingleTaxRateInTheList(): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->index(Resources::TAX_RATES)), 1); + } + + /** + * @Then I should be notified that tax rate with this code already exists + */ + public function iShouldBeNotifiedThatTaxRateWithThisCodeAlreadyExists(): void + { + $response = $this->client->getLastResponse(); + Assert::false( + $this->responseChecker->isCreationSuccessful($response), + 'Tax rate has been created successfully, but it should not', + ); + Assert::same( + $this->responseChecker->getError($response), + 'code: The tax rate with given code already exists.', + ); + } + + /** + * @Then there should still be only one tax rate with code :code + */ + public function thereShouldStillBeOnlyOneTaxRateWithCode(string $code): void + { + Assert::count( + $this->responseChecker->getCollectionItemsWithValue($this->client->index(Resources::TAX_RATES), 'code', $code), + 1, + sprintf('There is more than one tax rate with code %s', $code), + ); + } + + /** + * @Then I should be notified that :element is required + */ + public function iShouldBeNotifiedThatElementIsRequired(string $element): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('%s: Please enter tax rate %s.', $element, $element), + ); + } + + /** + * @Then tax rate with :element :code should not be added + */ + public function taxRateWithCodeShouldNotBeAdded(string $element, string $code): void + { + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::TAX_RATES), $element, $code), + sprintf('Tax rate with %s %s exist', $element, $code), + ); + } + + /** + * @Then I should be notified that zone has to be selected + */ + public function iShouldBeNotifiedThatZoneHasToBeSelected(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'zone: Please select tax zone.', + ); + } + + /** + * @Then I should be notified that category has to be selected + */ + public function iShouldBeNotifiedThatCategoryHasToBeSelected(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'category: Please select tax category.', + ); + } + + /** + * @Then I should not see a tax rate with name :name + */ + public function iShouldNotSeeATaxRateWithName(string $name): void + { + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->getLastResponse(), 'name', $name), + sprintf('Tax rate with name %s exists', $name), + ); + } + + /** + * @Then /^(this tax rate) should still be named "([^"]+)"$/ + * @Then /^(this tax rate) name should be "([^"]*)"$/ + */ + public function thisTaxRateShouldStillBeNamed(TaxRateInterface $taxRate, string $taxRateName): void + { + Assert::true( + $this->responseChecker->hasValue($this->client->show(Resources::TAX_RATES, (string) $taxRate->getCode()), 'name', $taxRateName), + sprintf('Tax rate name is not %s', $taxRateName), + ); + } + + /** + * @Then the code field should be disabled + * @Then I should not be able to edit its code + */ + public function iShouldNotBeAbleToEditItsCode(): void + { + $this->client->updateRequestData(['code' => 'NEW_CODE']); + + Assert::false($this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE')); + } + + /** + * @Then /^(this tax rate) amount should be ([^"]+)%$/ + */ + public function thisTaxRateAmountShouldBe(TaxRateInterface $taxRate, int $taxRateAmount): void + { + Assert::true( + $this->responseChecker->hasValue($this->client->show(Resources::TAX_RATES, (string) $taxRate->getCode()), 'amount', $taxRateAmount), + sprintf('Tax rate amount is not %s', $taxRateAmount), + ); + } + + /** + * @Then /^(this tax rate) should be applicable for the ("[^"]+" tax category)$/ + */ + public function thisTaxRateShouldBeApplicableForTheTaxCategory(TaxRateInterface $taxRate, TaxCategoryInterface $taxCategory): void + { + Assert::true( + $this->responseChecker->hasValue($this->client->show(Resources::TAX_RATES, (string) $taxRate->getCode()), 'category', $this->iriConverter->getIriFromResource($taxCategory)), + sprintf('Tax rate is not applicable for %s tax category', $taxCategory), + ); + } + + /** + * @Then /^(this tax rate) should be applicable in ("[^"]+" zone)$/ + */ + public function thisTaxRateShouldBeApplicableInZone(TaxRateInterface $taxRate, ZoneInterface $zone): void + { + Assert::true( + $this->responseChecker->hasValue($this->client->show(Resources::TAX_RATES, (string) $taxRate->getCode()), 'zone', $this->iriConverter->getIriFromResource($zone)), + sprintf('Tax rate is not applicable for %s zone', $zone), + ); + } + + /** + * @Then I should be notified that amount is invalid + */ + public function iShouldBeNotifiedThatAmountIsInvalid(): void + { + Assert::true( + $this->responseChecker->hasViolationWithMessage( + $this->client->getLastResponse(), + 'The tax rate amount is invalid.', + 'amount', + ), + ); + } + + /** + * @Then I should be notified that tax rate should not end before it starts + */ + public function iShouldBeNotifiedThatTaxRateShouldNotEndBeforeItStarts(): void + { + Assert::true( + $this->responseChecker->hasViolationWithMessage( + $this->client->getLastResponse(), + 'The tax rate should not end before it starts', + 'endDate', + ), + ); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php new file mode 100644 index 0000000000..14e025ff8c --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonImagesContext.php @@ -0,0 +1,166 @@ +getCode()), + Request::METHOD_POST, + ); + $builder->withHeader('CONTENT_TYPE', 'multipart/form-data'); + $builder->withHeader('HTTP_ACCEPT', 'application/ld+json'); + $builder->withHeader('HTTP_Authorization', 'Bearer ' . $this->sharedStorage->get('token')); + $builder->withFile('file', new UploadedFile($this->minkParameters['files_path'] . $path, basename($path))); + + if (null !== $type) { + $builder->withParameter('type', $type); + } + + $this->client->request($builder->build()); + } + + /** + * @When /^I attach the "([^"]+)" image to (this taxon)$/ + */ + public function iAttachTheImageToThisTaxon(string $path, TaxonInterface $taxon): void + { + $this->iAttachTheImageWithTypeToThisTaxon($path, null, $taxon); + } + + /** + * @When I( also) remove an image with :type type + */ + public function iRemoveAnImageWithType(string $type): void + { + /** @var TaxonInterface $taxon */ + $taxon = $this->sharedStorage->get('taxon'); + + $taxonImage = $taxon->getImagesByType($type)->first(); + Assert::notFalse($taxonImage); + + $this->client->delete(Resources::TAXON_IMAGES, (string) $taxonImage->getId()); + } + + /** + * @When I remove the first image + */ + public function iRemoveTheFirstImage(): void + { + /** @var TaxonInterface $taxon */ + $taxon = $this->sharedStorage->get('taxon'); + + $taxonImage = $taxon->getImages()->first(); + Assert::notFalse($taxonImage); + + $this->client->delete(Resources::TAXON_IMAGES, (string) $taxonImage->getId()); + } + + /** + * @When I change the first image type to :type + */ + public function iChangeTheFirstImageTypeTo(string $type): void + { + /** @var TaxonInterface $taxon */ + $taxon = $this->sharedStorage->get('taxon'); + + $taxonImage = $taxon->getImages()->first(); + Assert::notFalse($taxonImage); + + $this->client->buildUpdateRequest(Resources::TAXON_IMAGES, (string) $taxonImage->getId()); + $this->client->updateRequestData(['type' => $type]); + $this->client->update(); + } + + /** + * @Then I should be notified that the changes have been successfully applied + */ + public function iShouldBeNotifiedThatTheChangesHaveBeenSuccessfullyApplied(): void + { + $response = $this->client->getLastResponse(); + Assert::true( + $this->responseChecker->isDeletionSuccessful($response) || $this->responseChecker->isUpdateSuccessful($response), + ); + } + + /** + * @Then /^(this taxon) should(?:| also) have an image with "([^"]*)" type$/ + * @Then /^(it) should(?:| also) have an image with "([^"]*)" type$/ + */ + public function thisTaxonShouldHaveAnImageWithType(TaxonInterface $taxon, string $type): void + { + Assert::true($this->responseChecker->hasValuesInAnySubresourceObjectCollection( + $this->client->show(Resources::TAXONS, $taxon->getCode()), + 'images', + ['type' => $type], + )); + } + + /** + * @Then /^(this taxon) should not have(?:| also) any images with "([^"]*)" type$/ + * @Then /^(it) should not have(?:| also) any images with "([^"]*)" type$/ + */ + public function thisTaxonShouldNotHaveAnyImagesWithType(TaxonInterface $taxon, string $type): void + { + Assert::false($this->responseChecker->hasValuesInAnySubresourceObjectCollection( + $this->client->show(Resources::TAXONS, $taxon->getCode()), + 'images', + ['type' => $type], + )); + } + + /** + * @Then /^(this taxon) should have only one image$/ + * @Then /^(this taxon) should(?:| still) have (\d+) images?$/ + */ + public function thisTaxonShouldHaveImages(TaxonInterface $taxon, int $count = 1): void + { + Assert::count( + $this->responseChecker->getValue($this->client->show(Resources::TAXONS, $taxon->getCode()), 'images'), + $count, + ); + } + + /** + * @Then /^(this taxon) should not have any images$/ + */ + public function thisTaxonShouldNotHaveAnyImages(TaxonInterface $taxon): void + { + $this->thisTaxonShouldHaveImages($taxon, 0); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonsContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonsContext.php index b18bf096b8..359db72deb 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonsContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingTaxonsContext.php @@ -14,79 +14,369 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; use Behat\Behat\Context\Context; +use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\Resources; +use Sylius\Behat\Service\Converter\SectionAwareIriConverterInterface; +use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Component\Core\Formatter\StringInflector; +use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\TaxonInterface; -use Symfony\Component\BrowserKit\AbstractBrowser; -use Symfony\Component\BrowserKit\Cookie; -use Symfony\Component\HttpFoundation\RequestStack; use Webmozart\Assert\Assert; final class ManagingTaxonsContext implements Context { public function __construct( - private AbstractBrowser $client, - private RequestStack $requestStack, + private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, + private SectionAwareIriConverterInterface $sectionAwareIriConverter, + private SharedStorageInterface $sharedStorage, ) { } /** - * @When I look for a taxon with :phrase in name + * @When I want to see all taxons in store */ - public function iTypeIn($phrase): void + public function iWantToSeeAllTaxonsInStore(): void { - $this->client->getCookieJar()->set(new Cookie($this->requestStack->getSession()->getName(), $this->requestStack->getSession()->getId())); - $this->client->request('GET', '/admin/ajax/taxons/search', ['phrase' => $phrase], [], ['ACCEPT' => 'application/json']); + $this->client->index(Resources::TAXONS); } /** - * @When I want to get taxon with :code code + * @When I want to create a new taxon */ - public function iWantToGetTaxonWithCode($code): void + public function iWantToCreateNewTaxon(): void { - $this->client->getCookieJar()->set(new Cookie($this->requestStack->getSession()->getName(), $this->requestStack->getSession()->getId())); - $this->client->request('GET', '/admin/ajax/taxons/leaf', ['code' => $code], [], ['ACCEPT' => 'application/json']); + $this->client->buildCreateRequest(Resources::TAXONS); } /** - * @When /^I want to get children from (taxon "[^"]+")/ + * @When I want to create a new taxon for :parentTaxon */ - public function iWantToGetChildrenFromTaxon(TaxonInterface $taxon): void + public function iWantToCreateANewTaxonForParent(TaxonInterface $parentTaxon): void { - $this->client->getCookieJar()->set(new Cookie($this->requestStack->getSession()->getName(), $this->requestStack->getSession()->getId())); - $this->client->request('GET', '/admin/ajax/taxons/leafs', ['parentCode' => $taxon->getCode()], [], ['ACCEPT' => 'application/json']); + $this->iWantToCreateNewTaxon(); + $this->iSetItsParentTaxonTo($parentTaxon); } /** - * @When I want to get taxon root + * @When I want to modify the :taxon taxon */ - public function iWantToGetTaxonRoot(): void + public function iWantToModifyATaxon(TaxonInterface $taxon): void { - $this->client->getCookieJar()->set(new Cookie($this->requestStack->getSession()->getName(), $this->requestStack->getSession()->getId())); - $this->client->request('GET', '/admin/ajax/taxons/root-nodes', [], [], ['ACCEPT' => 'application/json']); + $this->sharedStorage->set('taxon', $taxon); + + $this->client->buildUpdateRequest(Resources::TAXONS, $taxon->getCode()); } /** - * @Then /^I should see (\d+) taxons on the list$/ + * @When I specify its code as :code + * @When I do not specify its code */ - public function iShouldSeeTaxonsInTheList($number): void + public function iSpecifyItsCodeAs(?string $code = null): void { - $response = $this->responseChecker->getResponseContent($this->client->getResponse()); - - Assert::eq(count($response), $number); + if ($code !== null) { + $this->client->addRequestData('code', $code); + } } /** - * @Then I should see the taxon named :firstName in the list - * @Then I should see the taxon named :firstName and :secondName in the list - * @Then I should see the taxon named :firstName, :secondName and :thirdName in the list - * @Then I should see the taxon named :firstName, :secondName, :thirdName and :fourthName in the list + * @When I name it :name in :localeCode + * @When I rename it to :name in :localeCode + * @When I do not specify its name */ - public function iShouldSeeTheTaxonNamedAnd(...$expectedTaxonNames): void + public function iNameItIn(?string $name = null, string $localeCode = 'en_US'): void { - $response = $this->responseChecker->getResponseContent($this->client->getResponse()); - $taxonNames = array_column($response, 'name'); + $this->updateTranslations($localeCode, 'name', $name); + } - Assert::allOneOf($expectedTaxonNames, $taxonNames); + /** + * @When I set its slug to :slug in :localeCode + */ + public function iSetItsSlugTo(string $slug, string $localeCode): void + { + $this->updateTranslations($localeCode, 'slug', $slug); + } + + /** + * @When I enable slug modification + * @When I enable slug modification in :localeCode + */ + public function iEnableSlugModification(string $localeCode = 'en_US'): void + { + $this->updateTranslations($localeCode, 'slug', ''); + } + + /** + * @When I describe it as :description in :localeCode + * @When I change its description to :description in :localeCode + */ + public function iDescribeItAsIn(string $description, string $localeCode): void + { + $this->updateTranslations($localeCode, 'description', $description); + } + + /** + * @When I set its parent taxon to :parentTaxon + * @When I change its parent taxon to :parentTaxon + */ + public function iSetItsParentTaxonTo(TaxonInterface $parentTaxon): void + { + $this->client->addRequestData('parent', $this->sectionAwareIriConverter->getIriFromResourceInSection($parentTaxon, 'admin')); + } + + /** + * @When /^I (enable|disable) it$/ + */ + public function iEnableIt(string $toggleAction): void + { + $this->client->addRequestData('enabled', $toggleAction === 'enable'); + } + + /** + * @When I (try to) add it + */ + public function iAddIt(): void + { + $this->client->create(); + } + + /** + * @When I remove taxon named :name + * @When I delete taxon named :name + * @When I try to delete taxon named :name + */ + public function iRemoveTaxonNamed(string $name): void + { + $code = StringInflector::nameToLowercaseCode($name); + + $this->client->delete(Resources::TAXONS, $code); + } + + /** + * @When I move down :taxonName taxon + */ + public function iMoveDownTaxon(string $taxonName): void + { + $lastResponse = $this->client->getLastResponse(); + $code = StringInflector::nameToLowercaseCode($taxonName); + + $taxon = $this->responseChecker->getCollectionItemsWithValue($lastResponse, 'code', $code); + $position = $taxon[0]['position']; + + $this->client->buildUpdateRequest(Resources::TAXONS, $code); + $this->client->addRequestData('position', $position + 1); + + $this->client->update(); + } + + /** + * @Then I should be notified that it has been successfully created + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyCreated(): void + { + Assert::true( + $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), + 'Taxon could not be created', + ); + } + + /** + * @Then I should see the taxon named :name in the list + */ + public function iShouldSeeTheTaxonNamedInTheList(string $name): void + { + $code = StringInflector::nameToLowercaseCode($name); + + Assert::true( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::TAXONS), 'code', $code), + ); + } + + /** + * @Then /^taxon named "([^"]+)" should not be added$/ + * @Then the taxon named :name should no longer exist in the registry + */ + public function taxonNamedShouldNotBeAdded(string $name): void + { + $code = StringInflector::nameToLowercaseCode($name); + + Assert::false( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::TAXONS), 'code', $code), + ); + } + + /** + * @Then /^the ("[^"]+" taxon) should appear in the registry$/ + */ + public function theTaxonShouldAppearInTheRegistry(TaxonInterface $taxon): void + { + Assert::true( + $this->responseChecker->hasItemWithValue($this->client->index(Resources::TAXONS), 'code', $taxon->getCode()), + ); + + $this->sharedStorage->set('taxon', $taxon); + } + + /** + * @Then I should be notified that I cannot delete a menu taxon of any channel + */ + public function iShouldBeNotifiedThatICannotDeleteAMenuTaxonOfAnyChannel(): void + { + $lastResponse = $this->client->getLastResponse(); + + Assert::false($this->responseChecker->isDeletionSuccessful($lastResponse)); + } + + /** + * @Then /^(it) should not belong to any other taxon$/ + */ + public function itShouldNotBelongToAnyOtherTaxon(TaxonInterface $taxon): void + { + Assert::true($this->responseChecker->hasItemWithValues( + $this->client->getLastResponse(), + [ + 'code' => $taxon->getCode(), + 'parent' => null, + ], + )); + } + + /** + * @Then /^(this taxon) should (belongs to "[^"]+")$/ + */ + public function thisTaxonShouldBelongsTo(TaxonInterface $taxon, TaxonInterface $parentTaxon): void + { + $this->iWantToSeeAllTaxonsInStore(); + + Assert::true($this->responseChecker->hasItemWithValues( + $this->client->getLastResponse(), + [ + 'code' => $taxon->getCode(), + 'parent' => $this->sectionAwareIriConverter->getIriFromResourceInSection($parentTaxon, 'admin'), + ], + )); + } + + /** + * @Then I should see :count taxons on the list + */ + public function iShouldSeeTaxonsInTheList(int $count): void + { + Assert::same($this->responseChecker->countCollectionItems($this->client->getLastResponse()), $count); + } + + /** + * @Then this taxon :field should be :value + * @Then this taxon should have :field :value in :localeCode + */ + public function thisTaxonFieldShouldBe(string $field, string $value, string $localeCode = 'en_US'): void + { + Assert::true($this->responseChecker->hasTranslation($this->client->getLastResponse(), $localeCode, $field, $value)); + } + + /** + * @Then the :field of the :taxonName taxon should( still) be :value + */ + public function theFieldOfTheTaxonShouldStillBe(string $field, string $taxonName, string $value): void + { + $this->thisTaxonFieldShouldBe($field, $value); + } + + /** + * @Then I should not be able to edit its code + */ + public function iShouldNotBeAbleToEditItsCode(): void + { + $this->client->updateRequestData(['code' => 'NEW_CODE']); + + Assert::false( + $this->responseChecker->hasValue($this->client->update(), 'code', 'NEW_CODE'), + 'The code field with value NEW_CODE exist', + ); + } + + /** + * @Then /^(it) should be (enabled|disabled)$/ + */ + public function itShouldBeDisabled(TaxonInterface $taxon, string $enabled): void + { + Assert::true($this->responseChecker->hasValue( + $this->client->show(Resources::TAXONS, $taxon->getCode()), + 'enabled', + $enabled === 'enabled', + )); + } + + /** + * @Then I should be notified that :field is required + */ + public function iShouldBeNotifiedThatFieldIsRequired(string $field): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + sprintf('Please enter taxon %s.', $field), + ); + } + + /** + * @Then I should be notified that taxon slug must be unique + */ + public function iShouldBeNotifiedThatTaxonSlugMustBeUnique(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'slug: Taxon slug must be unique.', + ); + } + + /** + * @Then I should be notified that taxon with this code already exists + */ + public function iShouldBeNotifiedThatTaxonWithThisCodeAlreadyExists(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'code: Taxon with given code already exists.', + ); + } + + /** + * @Then there should still be only one taxon with code :code + */ + public function thereShouldStillBeOnlyOneTaxonWithCode(string $code): void + { + Assert::count( + $this->responseChecker->getCollectionItemsWithValue($this->client->index(Resources::TAXONS), 'code', $code), + 1, + sprintf('There should be only one taxon with code "%s"', $code), + ); + } + + /** + * @Then the product :product should no longer have a main taxon + */ + public function theProductShouldNoLongerHaveAMainTaxon(ProductInterface $product): void + { + Assert::null($product->getMainTaxon()); + } + + /** + * @When I save my changes to the images + */ + public function iSaveMyChangesToTheImages(): void + { + // Intentionally left blank + } + + private function updateTranslations(string $localeCode, string $field, ?string $value = null): void + { + $data['translations'][$localeCode] = []; + + if ($value !== null) { + $data['translations'][$localeCode][$field] = $value; + } + + $this->client->updateRequestData($data); } } diff --git a/src/Sylius/Behat/Context/Api/Admin/ManagingZonesContext.php b/src/Sylius/Behat/Context/Api/Admin/ManagingZonesContext.php index e5ae013333..a51b7ebc37 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ManagingZonesContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ManagingZonesContext.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Admin; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; @@ -134,29 +134,6 @@ final class ManagingZonesContext implements Context $this->client->delete(Resources::ZONES, $zone->getCode()); } - /** - * @When I check (also) the :zone zone - */ - public function iCheckTheZone(ZoneInterface $zone): void - { - $ZoneToDelete = []; - if ($this->sharedStorage->has('zone_to_delete')) { - $ZoneToDelete = $this->sharedStorage->get('zone_to_delete'); - } - $ZoneToDelete[] = $zone->getCode(); - $this->sharedStorage->set('zone_to_delete', $ZoneToDelete); - } - - /** - * @When I delete them - */ - public function iDeleteThem(): void - { - foreach ($this->sharedStorage->get('zone_to_delete') as $code) { - $this->client->delete(Resources::ZONES, $code); - } - } - /** * @When I want to modify the zone named :zone */ @@ -201,14 +178,6 @@ final class ManagingZonesContext implements Context $this->removeZoneMember($zone); } - /** - * @When I save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @When I add the country :country again */ @@ -420,20 +389,8 @@ final class ManagingZonesContext implements Context ); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true( - $this->responseChecker->isUpdateSuccessful($this->client->getLastResponse()), - 'Zone could not be edited', - ); - } - /** * @Then I should be notified that it has been successfully deleted - * @Then I should be notified that they have been successfully deleted */ public function iShouldBeNotifiedThatItHasBeenSuccessfullyDeleted(): void { diff --git a/src/Sylius/Behat/Context/Api/Admin/RemovingTaxonContext.php b/src/Sylius/Behat/Context/Api/Admin/RemovingTaxonContext.php new file mode 100644 index 0000000000..e0e1c6a9a3 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Admin/RemovingTaxonContext.php @@ -0,0 +1,62 @@ +client->delete(Resources::TAXONS, $taxon->getCode()); + $this->sharedStorage->set('taxon', $taxon); + } + + /** + * @Then /^(this taxon) should still exist$/ + */ + public function theTaxonShouldStillExist(TaxonInterface $taxon): void + { + $this->client->show(Resources::TAXONS, $taxon->getCode()); + + Assert::true($this->responseChecker->isShowSuccessful($this->client->getLastResponse())); + } + + /** + * @Then I should be notified that this taxon could not be deleted as it is in use by a promotion rule + */ + public function iShouldBeNotifiedThatThisTaxonCouldNotBeDeleted(): void + { + Assert::contains( + $this->responseChecker->getError($this->client->getLastResponse()), + 'Cannot delete a taxon that is in use by a promotion rule.', + ); + } +} diff --git a/src/Sylius/Behat/Context/Api/Admin/ResettingPasswordContext.php b/src/Sylius/Behat/Context/Api/Admin/ResettingPasswordContext.php index 0cf6f70287..a583af37b8 100644 --- a/src/Sylius/Behat/Context/Api/Admin/ResettingPasswordContext.php +++ b/src/Sylius/Behat/Context/Api/Admin/ResettingPasswordContext.php @@ -115,9 +115,9 @@ final class ResettingPasswordContext implements Context $lastResponse = $this->client->getLastResponse(); - Assert::same($lastResponse->getStatusCode(), Response::HTTP_INTERNAL_SERVER_ERROR); + Assert::same($lastResponse->getStatusCode(), Response::HTTP_NOT_FOUND); $message = $this->responseChecker->getError($lastResponse); - Assert::startsWith($message, 'Internal Server Error'); + Assert::startsWith($message, 'No user found with reset token:'); } /** diff --git a/src/Sylius/Behat/Context/Api/Common/ResponseContext.php b/src/Sylius/Behat/Context/Api/Common/ResponseContext.php new file mode 100644 index 0000000000..dbb8e49ad3 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Common/ResponseContext.php @@ -0,0 +1,65 @@ +responseChecker->isUpdateSuccessful($this->client->getLastResponse()), + sprintf( + 'Resource could not be edited: %s', + $this->responseChecker->getError($this->client->getLastResponse()), + ), + ); + } + + /** + * @Then I should be notified that it has been successfully uploaded + */ + public function iShouldBeNotifiedThatItHasBeenSuccessfullyUploaded(): void + { + Assert::true( + $this->responseChecker->isCreationSuccessful($this->client->getLastResponse()), + sprintf( + 'Resource could not be created: %s', + $this->responseChecker->getError($this->client->getLastResponse()), + ), + ); + } + + /** + * @Then I should be notified that I can no longer change payment method of this order + */ + public function iShouldBeNotifiedThatICanNoLongerChangePaymentMethodOfThisOrder(): void + { + Assert::true($this->responseChecker->hasViolationWithMessage( + $this->client->getLastResponse(), + 'You cannot change the payment method for a cancelled order.', + )); + } +} diff --git a/src/Sylius/Behat/Context/Api/Common/SaveContext.php b/src/Sylius/Behat/Context/Api/Common/SaveContext.php new file mode 100644 index 0000000000..083876e1e0 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Common/SaveContext.php @@ -0,0 +1,33 @@ +client->update(); + } +} diff --git a/src/Sylius/Behat/Context/Api/EmailContext.php b/src/Sylius/Behat/Context/Api/EmailContext.php index e2854716ff..5a6ed7250c 100644 --- a/src/Sylius/Behat/Context/Api/EmailContext.php +++ b/src/Sylius/Behat/Context/Api/EmailContext.php @@ -42,6 +42,28 @@ final class EmailContext implements Context ); } + /** + * @Then a verification email should have been sent to :recipient + */ + public function aVerificationEmailShouldHaveBeenSentTo(string $recipient): void + { + $this->assertEmailContainsMessageTo( + $this->translator->trans('sylius.email.user.account_verification.message'), + $recipient, + ); + } + + /** + * @Then a welcoming email should not have been sent to :recipient + */ + public function aWelcomingEmailShouldNotHaveBeenSentTo(string $recipient): void + { + $this->assertEmailDoesNotContainMessageTo( + $this->translator->trans('sylius.email.user_registration.welcome_to_our_store'), + $recipient, + ); + } + /** * @Then :count email(s) should be sent to :recipient */ @@ -94,6 +116,25 @@ final class EmailContext implements Context ); } + /** + * @Then an email with the confirmation of the order :order should not be sent to :email + */ + public function anEmailWithTheConfirmationOfTheOrderShouldNotBeSentTo( + OrderInterface $order, + string $recipient, + string $localeCode = 'en_US', + ): void { + $this->assertEmailDoesNotContainMessageTo( + sprintf( + '%s %s %s', + $this->translator->trans('sylius.email.order_confirmation.your_order_number', [], null, $localeCode), + $order->getNumber(), + $this->translator->trans('sylius.email.order_confirmation.has_been_successfully_placed', [], null, $localeCode), + ), + $recipient, + ); + } + /** * @Then /^an email with shipment's details of (this order) should be sent to "([^"]+)"$/ * @Then /^an email with shipment's details of (this order) should be sent to "([^"]+)" in ("([^"]+)" locale)$/ @@ -169,11 +210,24 @@ final class EmailContext implements Context Assert::false($this->emailChecker->hasRecipient($recipient)); } + /** + * @Then only one email should have been sent to :recipient + */ + public function onlyOneEmailShouldHaveBeenSentTo(string $recipient): void + { + Assert::eq($this->emailChecker->countMessagesTo($recipient), 1); + } + private function assertEmailContainsMessageTo(string $message, string $recipient): void { Assert::true($this->emailChecker->hasMessageTo($message, $recipient)); } + private function assertEmailDoesNotContainMessageTo(string $message, string $recipient): void + { + Assert::false($this->emailChecker->hasMessageTo($message, $recipient)); + } + private function getShippingMethodName(OrderInterface $order): string { /** @var ShipmentInterface|false $shipment */ diff --git a/src/Sylius/Behat/Context/Api/Resources.php b/src/Sylius/Behat/Context/Api/Resources.php index 22671fe806..cb28dbe07e 100644 --- a/src/Sylius/Behat/Context/Api/Resources.php +++ b/src/Sylius/Behat/Context/Api/Resources.php @@ -17,12 +17,18 @@ final class Resources { public const ADDRESSES = 'addresses'; + public const ADJUSTMENTS = 'adjustments'; + public const ADMINISTRATORS = 'administrators'; public const AVATAR_IMAGES = 'avatar-images'; public const CATALOG_PROMOTIONS = 'catalog-promotions'; + public const CHANNEL_PRICE_HISTORY_CONFIGS = 'channel-price-history-configs'; + + public const CHANNEL_PRICING_LOG_ENTRIES = 'channel-pricing-log-entries'; + public const CHANNELS = 'channels'; public const CONTACT_REQUESTS = 'contact-requests'; @@ -41,7 +47,7 @@ final class Resources public const ORDER_ITEM_UNITS = 'order-item-units'; - public const ORDER_ITEMS = 'order-item-units'; + public const ORDER_ITEMS = 'order-items'; public const ORDERS = 'orders'; @@ -51,16 +57,26 @@ final class Resources public const PRODUCT_ASSOCIATION_TYPES = 'product-association-types'; + public const PRODUCT_ASSOCIATIONS = 'product-associations'; + + public const PRODUCT_ATTRIBUTES = 'product-attributes'; + + public const PRODUCT_IMAGES = 'product-images'; + public const PRODUCT_OPTIONS = 'product-options'; public const PRODUCT_REVIEWS = 'product-reviews'; + public const PRODUCT_TAXONS = 'product-taxons'; + public const PRODUCT_VARIANTS = 'product-variants'; public const PRODUCTS = 'products'; public const PROMOTIONS = 'promotions'; + public const PROMOTION_COUPONS = 'promotion-coupons'; + public const PROVINCES = 'provinces'; public const SHIPMENTS = 'shipments'; @@ -69,8 +85,14 @@ final class Resources public const SHIPPING_METHODS = 'shipping-methods'; + public const TAXON_IMAGES = 'taxon-images'; + + public const TAXONS = 'taxons'; + public const TAX_CATEGORIES = 'tax-categories'; + public const TAX_RATES = 'tax-rates'; + public const ZONES = 'zones'; private function __construct() diff --git a/src/Sylius/Behat/Context/Api/Shop/AddressContext.php b/src/Sylius/Behat/Context/Api/Shop/AddressContext.php index 51da425419..edd1483c2b 100644 --- a/src/Sylius/Behat/Context/Api/Shop/AddressContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/AddressContext.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Shop; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; @@ -266,7 +266,7 @@ final class AddressContext implements Context */ public function iTryToViewDetailsOfAddressBelongingTo(AddressInterface $address): void { - $this->client->showByIri($this->iriConverter->getIriFromItem($address)); + $this->client->showByIri($this->iriConverter->getIriFromResource($address)); } /** diff --git a/src/Sylius/Behat/Context/Api/Shop/CartContext.php b/src/Sylius/Behat/Context/Api/Shop/CartContext.php index cf4f9196ef..a7ff1cfd7e 100644 --- a/src/Sylius/Behat/Context/Api/Shop/CartContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/CartContext.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Shop; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\RequestFactoryInterface; @@ -116,6 +116,7 @@ final class CartContext implements Context /** * @When /^I add ("[^"]+" variant) of (this product) to the (cart)$/ * @When /^I add ("[^"]+" variant) of (product "[^"]+") to the (cart)$/ + * @When /^I have ("[^"]+" variant) of (product "[^"]+") in the (cart)$/ */ public function iAddVariantOfThisProductToTheCart( ProductVariantInterface $productVariant, @@ -123,6 +124,7 @@ final class CartContext implements Context ?string $tokenValue, ): void { $this->putProductVariantToCart($productVariant, $tokenValue, 1); + $this->sharedStorage->set('variant', $productVariant); } /** @@ -185,19 +187,25 @@ final class CartContext implements Context } /** - * @When /^I change (product "[^"]+") quantity to (\d+) in my (cart)$/ + * @Given /^I change (product "[^"]+") quantity to (\d+)$/ * @Given I change :productName quantity to :quantity + * @When /^I change (product "[^"]+") quantity to (\d+) in my (cart)$/ * @When /^the (?:visitor|customer) change (product "[^"]+") quantity to (\d+) in his (cart)$/ * @When /^the visitor try to change (product "[^"]+") quantity to (\d+) in the customer (cart)$/ * @When /^I try to change (product "[^"]+") quantity to (\d+) in my (cart)$/ */ - public function iChangeQuantityToInMyCart(ProductInterface $product, int $quantity, string $tokenValue): void + public function iChangeQuantityToInMyCart(ProductInterface $product, int $quantity, ?string $tokenValue = null): void { + if (null === $tokenValue && $this->sharedStorage->has('cart_token')) { + $tokenValue = $this->sharedStorage->get('cart_token'); + } + $itemResponse = $this->getOrderItemResponseFromProductInCart($product, $tokenValue); $this->changeQuantityOfOrderItem((string) $itemResponse['id'], $quantity, $tokenValue); } /** + * @Given /^I removed (product "[^"]+") from the (cart)$/ * @When /^I remove (product "[^"]+") from the (cart)$/ */ public function iRemoveProductFromTheCart(ProductInterface $product, string $tokenValue): void @@ -334,6 +342,20 @@ final class CartContext implements Context Assert::same($total, (int) $responseTotal, 'Expected totals are not the same. Received message:' . $response->getContent()); } + /** + * @Then /^my (cart) items total should be ("[^"]+")$/ + */ + public function myCartItemsTotalShouldBe(string $tokenValue, int $total): void + { + $response = $this->shopClient->show(Resources::ORDERS, $tokenValue); + $responseTotal = $this->responseChecker->getValue( + $response, + 'itemsSubtotal', + ); + + Assert::same($total, (int) $responseTotal, 'Expected items totals are not the same. Received message:' . $response->getContent()); + } + /** * @Then /^my included in price taxes should be ("[^"]+")$/ */ @@ -350,6 +372,7 @@ final class CartContext implements Context /** * @Then /^my (cart) should be empty$/ + * @Then /^(cart) should be empty with no value$/ */ public function myCartShouldBeEmpty(string $tokenValue): void { @@ -648,6 +671,17 @@ final class CartContext implements Context ); } + /** + * @Then /^my cart included in price taxes should be ("[^"]+")$/ + */ + public function myCartTaxesIncludedInPriceShouldBe(int $taxTotal): void + { + Assert::same( + $this->responseChecker->getValue($this->shopClient->getLastResponse(), 'taxIncludedTotal'), + $taxTotal, + ); + } + /** * @Then /^my cart should have (\d+) items of (product "([^"]+)")$/ * @Then /^my cart should have quantity of (\d+) items of (product "([^"]+)")$/ @@ -663,6 +697,8 @@ final class CartContext implements Context * @Then /^my cart shipping total should be ("[^"]+")$/ * @Then I should not see shipping total for my cart * @Then /^my cart estimated shipping cost should be ("[^"]+")$/ + * @Then there should be no shipping fee + * @Then my cart shipping should be for Free */ public function myCartShippingFeeShouldBe(int $shippingTotal = 0): void { @@ -708,31 +744,31 @@ final class CartContext implements Context } /** - * @Then /^(this product) should have ([^"]+) "([^"]+)"$/ + * @Then /^this product should have ([^"]+) "([^"]+)"$/ */ - public function thisItemShouldHaveOptionValue(ProductInterface $product, string $optionName, string $optionValue): void + public function thisItemShouldHaveOptionValue(string $expectedOptionName, string $expectedOptionValueValue): void { $item = $this->sharedStorage->get('item'); - $variantData = json_decode($this->shopClient->showByIri(urldecode($item['variant']))->getContent(), true, 512, \JSON_THROW_ON_ERROR); + $optionValues = $this->responseChecker->getValue($this->shopClient->showByIri($item['variant']), 'optionValues'); - foreach ($variantData['optionValues'] as $valueIri) { - $optionValueData = json_decode($this->shopClient->showByIri($valueIri)->getContent(), true, 512, \JSON_THROW_ON_ERROR); + foreach ($optionValues as $optionValueIri) { + $optionValue = $this->responseChecker->getResponseContent($this->shopClient->showByIri($optionValueIri)); - if ($optionValueData['value'] !== $optionValue) { + if ($optionValue['value'] !== $expectedOptionValueValue) { continue; } - $optionData = json_decode($this->shopClient->showByIri($optionValueData['option'])->getContent(), true, 512, \JSON_THROW_ON_ERROR); + $option = $this->responseChecker->getResponseContent($this->shopClient->showByIri($optionValue['option'])); - if ($optionData['name'] !== $optionName) { - continue; + if ($option['name'] === $expectedOptionName) { + return; } - - return; } - throw new \DomainException(sprintf('Could not find item with option "%s" set to "%s"', $optionName, $optionValue)); + throw new \DomainException( + sprintf('Could not find item with option "%s" set to "%s"', $expectedOptionName, $expectedOptionValueValue), + ); } /** @@ -801,7 +837,7 @@ final class CartContext implements Context 'items', ); $request->updateContent([ - 'productVariant' => $this->iriConverter->getIriFromItem($this->productVariantResolver->getVariant($product)), + 'productVariant' => $this->iriConverter->getIriFromResource($this->productVariantResolver->getVariant($product)), 'quantity' => $quantity, ]); @@ -820,7 +856,7 @@ final class CartContext implements Context 'items', ); $request->updateContent([ - 'productVariant' => $this->iriConverter->getIriFromItem($productVariant), + 'productVariant' => $this->iriConverter->getIriFromResource($productVariant), 'quantity' => $quantity, ]); diff --git a/src/Sylius/Behat/Context/Api/Shop/ChannelContext.php b/src/Sylius/Behat/Context/Api/Shop/ChannelContext.php index a30c9be154..27e144d759 100644 --- a/src/Sylius/Behat/Context/Api/Shop/ChannelContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/ChannelContext.php @@ -57,4 +57,36 @@ final class ChannelContext implements Context sprintf('%s/shop/currencies/%s', $this->apiUrlPrefix, $currencyCode), ); } + + /** + * @Then I should be able to shop using the :currencyCode currency + */ + public function iShouldBeAbleToShopUsingTheCurrency(string $currencyCode): void + { + $this->client->index(Resources::CURRENCIES); + + Assert::true( + $this->responseChecker->hasItemWithValue( + $this->client->getLastResponse(), + 'code', + $currencyCode, + ), + ); + } + + /** + * @Then I should not be able to shop using the :currencyCode currency + */ + public function iShouldNotBeAbleToShopUsingTheCurrency(string $currencyCode): void + { + $this->client->index(Resources::CURRENCIES); + + Assert::false( + $this->responseChecker->hasItemWithValue( + $this->client->getLastResponse(), + 'code', + $currencyCode, + ), + ); + } } diff --git a/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutCompleteContext.php b/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutCompleteContext.php new file mode 100644 index 0000000000..97219b1dbe --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Shop/Checkout/CheckoutCompleteContext.php @@ -0,0 +1,71 @@ +requestFactory->customItemAction( + 'shop', + Resources::ORDERS, + $this->sharedStorage->get('cart_token'), + HTTPRequest::METHOD_PATCH, + 'complete', + ); + + $this->client->executeCustomRequest($request); + } + + /** + * @Then /^I should be informed that (this variant) has been disabled$/ + */ + public function iShouldBeInformedThatThisVariantHasBeenDisabled(ProductVariantInterface $productVariant): void + { + $lastResponseContent = $this->client->getLastResponse()->getContent(); + + Assert::string($lastResponseContent); + Assert::contains($lastResponseContent, sprintf('This product %s has been disabled.', $productVariant->getName())); + } + + /** + * @Then my order should not be placed due to changed order total + */ + public function myOrderShouldNotBePlacedDueToChangedOrderTotal(): void + { + $lastResponseContent = $this->client->getLastResponse()->getContent(); + + Assert::string($lastResponseContent); + Assert::contains($lastResponseContent, 'Order total has changed during checkout process'); + } +} diff --git a/src/Sylius/Behat/Context/Api/Shop/CheckoutContext.php b/src/Sylius/Behat/Context/Api/Shop/CheckoutContext.php index 8872ebc322..b1b61e7801 100644 --- a/src/Sylius/Behat/Context/Api/Shop/CheckoutContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/CheckoutContext.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Shop; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\RequestFactoryInterface; @@ -347,7 +347,7 @@ final class CheckoutContext implements Context { $response = $this->completeOrder(); - if ($response->getStatusCode() === 422) { + if ($response->getStatusCode() > 299) { return; } @@ -377,6 +377,9 @@ final class CheckoutContext implements Context * @When /^the visitor try to proceed with ("[^"]+" shipping method) in the customer cart$/ * @When I try to change shipping method to :shippingMethod * @Given I proceed selecting :shippingMethod shipping method + * @Given I chose :shippingMethod shipping method + * @When I change shipping method to :shippingMethod + * @When I have proceeded selecting :shippingMethod shipping method */ public function iProceededWithShippingMethod(ShippingMethodInterface $shippingMethod): void { @@ -417,6 +420,14 @@ final class CheckoutContext implements Context $this->client->executeCustomRequest($request); } + /** + * @When I decide to change my address + */ + public function iDecideToChangeMyAddress(): void + { + // Intentionally left blank + } + /** * @Then I should be notified that the order should be addressed first */ @@ -466,12 +477,11 @@ final class CheckoutContext implements Context * @Given I completed the payment step with :paymentMethod payment method * @When I choose :paymentMethod payment method * @When I select :paymentMethod payment method - * @When I have proceeded selecting :paymentMethod payment method - * @When I proceed selecting :paymentMethod payment method * @When /^the (?:customer|visitor) proceed with ("[^"]+" payment)$/ * @Given /^the (?:customer|visitor) has proceeded ("[^"]+" payment)$/ * @When I try to change payment method to :paymentMethod payment * @When I change payment method to :paymentMethod after checkout + * @When I retry the payment with :paymentMethod payment method */ public function iChoosePaymentMethod(PaymentMethodInterface $paymentMethod): void { @@ -482,13 +492,14 @@ final class CheckoutContext implements Context HTTPRequest::METHOD_PATCH, \sprintf('payments/%s', $this->getCart()['payments'][0]['id']), ); - $request->setContent(['paymentMethod' => $this->iriConverter->getIriFromItem($paymentMethod)]); + $request->setContent(['paymentMethod' => $this->iriConverter->getIriFromResource($paymentMethod)]); $this->client->executeCustomRequest($request); } /** * @When I proceed through checkout process + * @When I proceeded through checkout process */ public function iProceedThroughCheckoutProcess(): void { @@ -501,6 +512,17 @@ final class CheckoutContext implements Context $this->iChoosePaymentMethod($paymentMethod); } + /** + * @When I proceed selecting :paymentMethod payment method + * @When I have proceeded selecting :paymentMethod payment method + */ + public function iHaveProceededSelectingPaymentMethod(PaymentMethodInterface $paymentMethod): void + { + $this->addressOrder($this->getArrayWithDefaultAddress()); + $this->iCompleteTheShippingStepWithFirstShippingMethod(); + $this->iChoosePaymentMethod($paymentMethod); + } + /** * @Given I have proceeded through checkout process with :shippingMethod shipping method */ @@ -547,6 +569,38 @@ final class CheckoutContext implements Context ); } + /** + * @Then I should be informed with :paymentMethod payment method instructions + */ + public function iShouldBeInformedWithPaymentMethodInstructions(PaymentMethodInterface $paymentMethod): void + { + $response = $this->client->getLastResponse(); + $payments = $this->responseChecker->getValue($response, 'payments'); + + Assert::notEmpty($payments, 'No payments found in response.'); + $paymentMethodIri = $this->iriConverter->getIriFromResource($paymentMethod); + foreach ($payments as $payment) { + if ($payment['method'] !== $paymentMethodIri) { + continue; + } + + $customRequest = $this->requestFactory->custom($payment['method'], HTTPRequest::METHOD_GET); + $paymentMethodResponse = $this->client->executeCustomRequest($customRequest); + Assert::same( + $this->responseChecker->getValue( + $paymentMethodResponse, + 'instructions', + ), + $paymentMethod->getInstructions(), + sprintf('Payment method instructions should be equal to %s', $paymentMethod->getInstructions()), + ); + + return; + } + + throw new \Exception(sprintf('Payment method %s not found in response.', $paymentMethod->getName())); + } + /** * @Then I should not be able to confirm order because products do not fit :shippingMethod requirements */ @@ -818,6 +872,14 @@ final class CheckoutContext implements Context Assert::same($this->getCheckoutState(), OrderCheckoutStates::STATE_COMPLETED); } + /** + * @Then I should see selected :shippingMethod shipping method + */ + public function iShouldSeeSelectedShippingMethod(ShippingMethodInterface $shippingMethod): void + { + Assert::true($this->hasShippingMethod($shippingMethod)); + } + /** * @Then I should not see :shippingMethod shipping method */ @@ -1006,7 +1068,7 @@ final class CheckoutContext implements Context { Assert::true($this->isViolationWithMessageInResponse( $this->client->getLastResponse(), - sprintf('The product variant with %s does not exist.', $productVariant->getCode()), + sprintf('The product variant %s does not exist.', $productVariant->getCode()), )); } @@ -1017,7 +1079,7 @@ final class CheckoutContext implements Context { Assert::true($this->isViolationWithMessageInResponse( $this->client->getLastResponse(), - sprintf('The product variant with %s does not exist.', $code), + sprintf('The product variant %s does not exist.', $code), )); } @@ -1440,7 +1502,7 @@ final class CheckoutContext implements Context 'items', ); $request->setContent([ - 'productVariant' => $this->iriConverter->getIriFromItem($productVariant), + 'productVariant' => $this->iriConverter->getIriFromResource($productVariant), 'quantity' => $quantity, ]); @@ -1523,7 +1585,7 @@ final class CheckoutContext implements Context HTTPRequest::METHOD_PATCH, sprintf('shipments/%s', $this->getCart()['shipments'][0]['id']), ); - $request->setContent(['shippingMethod' => $this->iriConverter->getIriFromItem($shippingMethod)]); + $request->setContent(['shippingMethod' => $this->iriConverter->getIriFromResource($shippingMethod)]); return $this->client->executeCustomRequest($request); } diff --git a/src/Sylius/Behat/Context/Api/Shop/CustomerContext.php b/src/Sylius/Behat/Context/Api/Shop/CustomerContext.php index 3263906aa1..646a7cb186 100644 --- a/src/Sylius/Behat/Context/Api/Shop/CustomerContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/CustomerContext.php @@ -96,14 +96,6 @@ final class CustomerContext implements Context $this->client->addRequestData('email', $email); } - /** - * @When I (try to) save my changes - */ - public function iSaveMyChanges(): void - { - $this->client->update(); - } - /** * @When I specify the current password as :password */ @@ -205,14 +197,6 @@ final class CustomerContext implements Context $this->loginContext->iLogInAsWithPassword($email, $password); } - /** - * @Then I should be notified that it has been successfully edited - */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void - { - Assert::true($this->responseChecker->isUpdateSuccessful($this->client->getLastResponse())); - } - /** * @Then I should be notified that the verification email has been sent */ diff --git a/src/Sylius/Behat/Context/Api/Shop/HomepageContext.php b/src/Sylius/Behat/Context/Api/Shop/HomepageContext.php index a9f5e8df48..c11abd5cb7 100644 --- a/src/Sylius/Behat/Context/Api/Shop/HomepageContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/HomepageContext.php @@ -13,10 +13,13 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Shop; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; +use Doctrine\Persistence\ObjectManager; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Symfony\Component\HttpFoundation\Request as HttpRequest; +use Symfony\Component\HttpFoundation\Response; use Webmozart\Assert\Assert; final class HomepageContext implements Context @@ -24,6 +27,8 @@ final class HomepageContext implements Context public function __construct( private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, + private IriConverterInterface $iriConverter, + private ObjectManager $objectManager, private string $apiUrlPrefix, ) { } @@ -72,6 +77,7 @@ final class HomepageContext implements Context */ public function iCheckAvailableTaxons(): void { + $this->objectManager->clear(); // avoiding doctrine cache $this->client->customAction(sprintf('%s/shop/taxons', $this->apiUrlPrefix), HttpRequest::METHOD_GET); } @@ -89,12 +95,12 @@ final class HomepageContext implements Context */ public function iShouldSeeAndInTheMenu(string ...$expectedMenuItems): void { - $response = json_decode($this->client->getLastResponse()->getContent(), true); - Assert::keyExists($response, 'hydra:member'); - $menuItems = array_column($response['hydra:member'], 'name'); + $menuItems = $this->getAvailableTaxonMenuItemsFromTaxonCollection($this->client->getLastResponse()); - Assert::notEmpty($menuItems); - Assert::allOneOf($menuItems, $expectedMenuItems); + Assert::true( + $this->areAllMenuItemsVisible($menuItems, $expectedMenuItems), + sprintf('Menu items %s should be present in the menu', implode(', ', $expectedMenuItems)), + ); } /** @@ -104,13 +110,42 @@ final class HomepageContext implements Context */ public function iShouldNotSeeAndInTheMenu(string ...$unexpectedMenuItems): void { - $response = json_decode($this->client->getLastResponse()->getContent(), true); - $menuItems = array_column($response, 'name'); + $menuItems = $this->getAvailableTaxonMenuItemsFromTaxonCollection($this->client->getLastResponse()); - foreach ($unexpectedMenuItems as $unexpectedMenuItem) { - if (in_array($unexpectedMenuItem, $menuItems, true)) { - throw new \InvalidArgumentException(sprintf('There is menu item %s but it should not be', $unexpectedMenuItem)); + Assert::false( + $this->areAllMenuItemsVisible($menuItems, $unexpectedMenuItems), + sprintf('Menu items %s should not be present in the menu', implode(', ', $unexpectedMenuItems)), + ); + } + + private function areAllMenuItemsVisible(array $menuItems, array $expectedMenuItems): bool + { + foreach ($expectedMenuItems as $expectedMenuItem) { + if (!in_array($expectedMenuItem, $menuItems)) { + return false; } } + + return true; + } + + private function getAvailableTaxonMenuItemsFromTaxonCollection(Response $response): array + { + $taxons = $this->responseChecker->getCollection($response); + if ([] === $taxons) { + return []; + } + $menuItems = array_column($taxons, 'name'); + + Assert::notEmpty($menuItems); + + $children = array_column($taxons, 'children'); + foreach ($children[0] as $child) { + if (!empty($child)) { + array_push($menuItems, $this->iriConverter->getResourceFromIri($child)->getName()); + } + } + + return $menuItems; } } diff --git a/src/Sylius/Behat/Context/Api/Shop/LoginContext.php b/src/Sylius/Behat/Context/Api/Shop/LoginContext.php index 122c40ce50..f201593540 100644 --- a/src/Sylius/Behat/Context/Api/Shop/LoginContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/LoginContext.php @@ -13,19 +13,22 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Shop; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ApiSecurityClientInterface; use Sylius\Behat\Client\RequestFactoryInterface; use Sylius\Behat\Client\RequestInterface; use Sylius\Behat\Client\ResponseCheckerInterface; +use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Locale\Model\LocaleInterface; use Symfony\Component\BrowserKit\AbstractBrowser; +use Symfony\Component\BrowserKit\Exception\BadMethodCallException; use Symfony\Component\HttpFoundation\Request as HTTPRequest; +use Symfony\Component\HttpFoundation\Response; use Webmozart\Assert\Assert; final class LoginContext implements Context @@ -204,7 +207,17 @@ final class LoginContext implements Context */ public function iShouldNotBeLoggedIn(): void { - Assert::false($this->apiSecurityClient->isLoggedIn(), 'Shop user should not be logged in, but they are.'); + try { + $isLoggedIn = $this->apiSecurityClient->isLoggedIn(); + } catch (BadMethodCallException) { + /** @var ShopUserInterface $shopUser */ + $shopUser = $this->sharedStorage->get('user'); + $this->client->show(Resources::CUSTOMERS, (string) $shopUser->getCustomer()->getId()); + + $isLoggedIn = $this->client->getLastResponse()->getStatusCode() !== Response::HTTP_UNAUTHORIZED; + } + + Assert::false($isLoggedIn, 'Shop user should not be logged in, but they are.'); } /** @@ -258,7 +271,7 @@ final class LoginContext implements Context $this->shopAuthenticationTokenClient->getResponse(), 'customer', ), - $this->iriConverter->getIriFromItem($customer), + $this->iriConverter->getIriFromResource($customer), ); } @@ -267,11 +280,18 @@ final class LoginContext implements Context */ public function iShouldNotBeAbleToChangeMyPasswordAgainWithTheSameToken(): void { - $this->client->executeCustomRequest($this->request); + $response = $this->client->executeCustomRequest($this->request); + Assert::same($response->getStatusCode(), 422); + Assert::same($this->responseChecker->getError($response), 'resetPasswordToken: Password reset token itotallyforgotmypassword is invalid.'); + } - // token is removed when used - Assert::same($this->client->getLastResponse()->getStatusCode(), 500); - $message = $this->responseChecker->getError($this->client->getLastResponse()); - Assert::startsWith($message, 'Internal Server Error'); + /** + * @Then I should not be able to change my password with this token + */ + public function iShouldNotBeAbleToChangeMyPasswordWithThisToken(): void + { + $response = $this->client->getLastResponse(); + Assert::same($response->getStatusCode(), 422); + Assert::same($this->responseChecker->getError($response), 'resetPasswordToken: Password reset token has expired.'); } } diff --git a/src/Sylius/Behat/Context/Api/Shop/OrderContext.php b/src/Sylius/Behat/Context/Api/Shop/OrderContext.php index dda96d25bc..64f5b00b62 100644 --- a/src/Sylius/Behat/Context/Api/Shop/OrderContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/OrderContext.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Shop; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\RequestFactoryInterface; @@ -48,6 +48,26 @@ final class OrderContext implements Context ) { } + /** + * @Given /^I am changing (this order)'s payment method$/ + */ + public function iWantToChangeThisOrdersPaymentMethod(OrderInterface $order): void + { + $request = $this->requestFactory->custom( + sprintf( + '%s/shop/account/orders/%s/payments/%s', + $this->apiUrlPrefix, + $order->getTokenValue(), + (string) $order->getPayments()->first()->getId(), + ), + HttpRequest::METHOD_PATCH, + [], + $this->shopClient->getToken(), + ); + + $this->sharedStorage->set('payment_method_patch_request', $request); + } + /** * @When I change my payment method to :paymentMethod */ @@ -67,7 +87,19 @@ final class OrderContext implements Context [], $this->shopClient->getToken(), ); - $request->setContent(['paymentMethod' => $this->iriConverter->getIriFromItem($paymentMethod)]); + $request->setContent(['paymentMethod' => $this->iriConverter->getIriFromResource($paymentMethod)]); + + $this->shopClient->executeCustomRequest($request); + } + + /** + * @When I try to change my payment method to :paymentMethod + */ + public function iTryToChangeMyPaymentMethodTo(PaymentMethodInterface $paymentMethod): void + { + $request = $this->sharedStorage->get('payment_method_patch_request'); + + $request->setContent(['paymentMethod' => $this->iriConverter->getIriFromResource($paymentMethod)]); $this->shopClient->executeCustomRequest($request); } @@ -176,7 +208,7 @@ final class OrderContext implements Context ): void { $resources = $this->responseChecker->getValue($this->shopClient->getLastResponse(), $elementType . 's'); - $resource = $this->iriConverter->getItemFromIri($resources[$position]['@id']); + $resource = $this->iriConverter->getResourceFromIri($resources[$position]['@id']); Assert::same(ucfirst($resource->getState()), $elementStatus); } @@ -308,7 +340,7 @@ final class OrderContext implements Context ->getValue($this->shopClient->show(Resources::ORDERS, $this->sharedStorage->get('cart_token')), 'payments')[0] ; - Assert::same($this->iriConverter->getIriFromItem($paymentMethod), $payment['method']); + Assert::same($this->iriConverter->getIriFromResource($paymentMethod), $payment['method']); } /** @@ -334,10 +366,10 @@ final class OrderContext implements Context { $paymentMethodIri = $this ->responseChecker - ->getValue($this->shopClient->getLastResponse(), 'payments')[0]['method']['@id'] + ->getValue($this->shopClient->getLastResponse(), 'payments')[0]['method'] ; - Assert::same($this->iriConverter->getItemFromIri($paymentMethodIri)->getCode(), $paymentMethod->getCode()); + Assert::same($this->iriConverter->getResourceFromIri($paymentMethodIri)->getCode(), $paymentMethod->getCode()); } /** diff --git a/src/Sylius/Behat/Context/Api/Shop/ProductAttributeContext.php b/src/Sylius/Behat/Context/Api/Shop/ProductAttributeContext.php index 53c5f0d467..6eef5b2521 100644 --- a/src/Sylius/Behat/Context/Api/Shop/ProductAttributeContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/ProductAttributeContext.php @@ -37,8 +37,15 @@ final class ProductAttributeContext implements Context public function iShouldSeeTheProductAttributeWithValue(string $attributeName, string $expectedAttribute): void { $attribute = $this->getAttributeByName($attributeName); + $attributeValue = $attribute['value']; - Assert::same($attribute['value'], $expectedAttribute); + if (is_array($attributeValue)) { + Assert::inArray($expectedAttribute, $attributeValue); + + return; + } + + Assert::same($attributeValue, $expectedAttribute); } /** @@ -71,6 +78,14 @@ final class ProductAttributeContext implements Context Assert::inArray($expectedAttribute, $attribute['value']); } + /** + * @Then I should not see the product attribute :attributeName + */ + public function iShouldNotSeeTheProductAttribute(string $attributeName): void + { + Assert::false($this->hasAttributeByName($attributeName)); + } + /** * @Then I should (also) see the product attribute :attributeName with date :expectedAttribute */ @@ -113,6 +128,17 @@ final class ProductAttributeContext implements Context Assert::same($attribute['name'], $name); } + private function hasAttributeByName(string $name): bool + { + foreach ($this->getAttributes() as $attribute) { + if ($attribute['name'] === $name) { + return true; + } + } + + return false; + } + private function getAttributeByName(string $name): array { foreach ($this->getAttributes() as $attribute) { diff --git a/src/Sylius/Behat/Context/Api/Shop/ProductContext.php b/src/Sylius/Behat/Context/Api/Shop/ProductContext.php index cb56acfa47..fb42a38f9c 100644 --- a/src/Sylius/Behat/Context/Api/Shop/ProductContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/ProductContext.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Shop; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Doctrine\Common\Collections\ArrayCollection; use Sylius\Behat\Client\ApiClientInterface; @@ -25,9 +25,11 @@ use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Model\TaxonInterface; +use Sylius\Component\Product\Model\ProductAssociationTypeInterface; use Sylius\Component\Product\Model\ProductVariantInterface; -use Sylius\Component\Taxonomy\Model\TaxonInterface; use Symfony\Component\HttpFoundation\Request as HttpRequest; +use Symfony\Component\HttpFoundation\Response; use Webmozart\Assert\Assert; final class ProductContext implements Context @@ -60,6 +62,14 @@ final class ProductContext implements Context $this->sharedStorage->remove('product_attributes'); } + /** + * @When I try to reach unexistent product + */ + public function iTryToReachUnexistentProduct(): void + { + $this->client->show(Resources::PRODUCTS, 'unexistent'); + } + /** * @When I view product :product in the :localeCode locale * @When /^I check (this product)'s details in the ("([^"]+)" locale)$/ @@ -103,7 +113,7 @@ final class ProductContext implements Context $this->client->index(Resources::PRODUCTS); if ($taxon !== null) { - $this->client->addFilter('taxon', $this->iriConverter->getIriFromItem($taxon)); + $this->client->addFilter('taxon', $this->iriConverter->getIriFromResource($taxon)); $this->client->filter(); } } @@ -248,7 +258,7 @@ final class ProductContext implements Context /** @var ProductVariantInterface $productVariant */ $productVariant = $this->sharedStorage->get('product_variant'); - $variantResponse = $this->client->showByIri($this->iriConverter->getIriFromItem($productVariant)); + $variantResponse = $this->client->showByIri($this->iriConverter->getIriFromResource($productVariant)); Assert::false($this->responseChecker->getValue($variantResponse, 'inStock')); } @@ -270,10 +280,12 @@ final class ProductContext implements Context */ public function iShouldSeeTheProductPrice(int $price): void { - Assert::true($this->hasProductWithPrice( - [$this->responseChecker->getResponseContent($this->client->getLastResponse())], - $price, - )); + /** @var ProductVariantInterface $checkedVariant */ + $checkedVariant = $this->sharedStorage->get('product_variant'); + $variant = $this->fetchItemByIri($this->iriConverter->getIriFromResource($checkedVariant)); + + Assert::same($variant['price'], $price); + Assert::same($variant['code'], $checkedVariant->getCode()); } /** @@ -576,9 +588,21 @@ final class ProductContext implements Context /** * @Then /^I should not be able to select the "([^"]+)" ([^\s]+) option value$/ */ - public function iShouldNotBeAbleToSelectTheOptionValue(string $optionValue, string $optionName): void + public function iShouldNotBeAbleToSelectTheOptionValue(string $optionValueValue, string $optionName): void { - Assert::false($this->hasProductOptionWithNameAndValue($optionValue, $optionName)); + Assert::false($this->hasProductOptionWithNameAndValue($optionName, $optionValueValue)); + } + + /** + * @Then /^I should be able to select the "([^"]+)" and "([^"]+)" ([^\s]+) option values$/ + */ + public function iShouldBeAbleToSelectTheAndColorOptionValues( + string $optionValueValue1, + string $optionValueValue2, + string $optionName, + ): void { + Assert::true($this->hasProductOptionWithNameAndValue($optionName, $optionValueValue1)); + Assert::true($this->hasProductOptionWithNameAndValue($optionName, $optionValueValue2)); } /** @@ -608,13 +632,88 @@ final class ProductContext implements Context */ public function iShouldSeeTheProductAssociationWithProductsAnd(string $productAssociationName, array $products): void { + Assert::true($this->isProductAssociationWithProductsAvailable($productAssociationName, $products)); + } + + /** + * @Then /^I should(?:| also) see the product association "([^"]+)" with (product "[^"]+")$/ + */ + public function iShouldSeeTheProductAssociationWithProduct(string $productAssociationName, ProductInterface $product): void + { + Assert::true($this->isProductAssociationWithProductsAvailable($productAssociationName, [$product])); + } + + /** + * @Then /^I should(?:| also) not see the product association "([^"]+)" with (product "[^"]+")$/ + */ + public function iShouldNotSeeTheProductAssociationWithProduct(string $productAssociationName, ProductInterface $product): void + { + Assert::false($this->isProductAssociationWithProductsAvailable($productAssociationName, [$product])); + } + + /** + * @Then /^I should not see the product (association "([^"]+)")$/ + */ + public function iShouldNotSeeTheProductAssociation(ProductAssociationTypeInterface $productAssociationType): void + { + $productAssociationTypeIri = $this->iriConverter->getIriFromResource($productAssociationType); + /** @var ProductInterface $product */ $product = $this->sharedStorage->get('product'); $response = $this->client->show(Resources::PRODUCTS, $product->getCode()); $associations = $this->responseChecker->getValue($response, 'associations'); - Assert::true($this->hasAssociationsWithProducts($associations, $productAssociationName, $products)); + foreach ($associations as $association) { + $associationResponse = $this->client->showByIri($association); + $associationTypeIri = $this->responseChecker->getValue($associationResponse, 'type'); + + Assert::notSame($associationTypeIri, $productAssociationTypeIri); + } + } + + /** + * @Then I should not see information about its lowest price + */ + public function iShouldNotSeeInformationAboutItsLowestPrice(): void + { + $product = $this->responseChecker->getResponseContent($this->client->getLastResponse()); + $variant = $this->responseChecker->getResponseContent( + $this->client->showByIri((string) $product['defaultVariant']), + ); + + Assert::keyExists($variant, 'lowestPriceBeforeDiscount'); + Assert::same($variant['lowestPriceBeforeDiscount'], null); + } + + /** + * @Then /^I should see ("[^"]+") as its lowest price before the discount$/ + */ + public function iShouldSeeAsItsLowestPriceBeforeTheDiscount(int $lowestPriceBeforeDiscount): void + { + $product = $this->responseChecker->getResponseContent($this->client->getLastResponse()); + $variant = $this->responseChecker->getResponseContent( + $this->client->showByIri((string) $product['defaultVariant']), + ); + + Assert::keyExists($variant, 'lowestPriceBeforeDiscount'); + Assert::same($variant['lowestPriceBeforeDiscount'], $lowestPriceBeforeDiscount); + } + + /** + * @Then I should be informed that the product does not exist + */ + public function iShouldBeInformedThatTheProductDoesNotExist(): void + { + Assert::same($this->client->getLastResponse()->getStatusCode(), Response::HTTP_NOT_FOUND); + } + + /** + * @Then /^I should be informed that the taxon does not exist$/ + */ + public function iShouldBeInformedThatTheTaxonDoesNotExist(): void + { + Assert::same($this->client->getLastResponse()->getStatusCode(), Response::HTTP_NOT_FOUND); } private function hasProductWithPrice( @@ -666,19 +765,21 @@ final class ProductContext implements Context return false; } - private function hasProductOptionWithNameAndValue(string $optionValue, string $optionName): bool + private function hasProductOptionWithNameAndValue(string $expectedOptionName, string $expectedOptionValueValue): bool { - $response = $this->client->getLastResponse(); - $productOptions = $this->responseChecker->getValue($response, 'options'); + $productVariants = $this->responseChecker->getCollection( + $this->client->index( + Resources::PRODUCT_VARIANTS, + ['product' => $this->iriConverter->getIriFromResource($this->sharedStorage->get('product'))], + ), + ); - foreach ($productOptions as $optionIri) { - if (!$this->hasProductOptionWithName($optionIri, $optionName)) { - continue; - } + foreach ($productVariants as $productVariant) { + foreach ($productVariant['optionValues'] as $optionValueIri) { + $optionValueData = $this->fetchItemByIri($optionValueIri); + $optionData = $this->fetchItemByIri($optionValueData['option']); - $variants = $this->responseChecker->getValue($response, 'variants'); - foreach ($variants as $variantIri) { - if ($this->variantHasProductOptionValue($variantIri, $optionValue)) { + if ($optionData['name'] === $expectedOptionName && $optionValueData['value'] === $expectedOptionValueValue) { return true; } } @@ -687,32 +788,6 @@ final class ProductContext implements Context return false; } - private function hasProductOptionWithName(string $optionIri, string $optionName): bool - { - $response = $this->client->showByIri($optionIri); - - return $this->responseChecker->hasValue($response, 'code', StringInflector::nameToUppercaseCode($optionName)); - } - - private function variantHasProductOptionValue(string $variantIri, string $optionValue): bool - { - $variants = $this->client->showByIri($variantIri); - $optionValues = $this->responseChecker->getValue($variants, 'optionValues'); - - foreach ($optionValues as $valueIri) { - if ($this->hasProductOptionWithValue($valueIri, $optionValue)) { - return true; - } - } - - return false; - } - - private function hasProductOptionWithValue(string $valueIri, string $optionValue): bool - { - return $this->responseChecker->hasValue($this->client->ShowByIri($valueIri), 'code', StringInflector::nameToUppercaseCode($optionValue)); - } - private function productHasProductVariantWithName(array $variants, string $variantName): bool { foreach ($variants as $variantIri) { @@ -770,8 +845,24 @@ final class ProductContext implements Context private function isProductAssociated(ProductInterface $product, array $associatedProducts): bool { - $productIri = $this->iriConverter->getIriFromItem($product); + $productIri = $this->iriConverter->getIriFromResource($product); return in_array($productIri, $associatedProducts, true); } + + private function fetchItemByIri(string $iri): array + { + return $this->responseChecker->getResponseContent($this->client->showByIri($iri)); + } + + private function isProductAssociationWithProductsAvailable(string $productAssociationName, array $associatedProducts): bool + { + /** @var ProductInterface $product */ + $product = $this->sharedStorage->get('product'); + + $response = $this->client->show(Resources::PRODUCTS, $product->getCode()); + $associations = $this->responseChecker->getValue($response, 'associations'); + + return $this->hasAssociationsWithProducts($associations, $productAssociationName, $associatedProducts); + } } diff --git a/src/Sylius/Behat/Context/Api/Shop/ProductReviewContext.php b/src/Sylius/Behat/Context/Api/Shop/ProductReviewContext.php index e2f15bec1f..f5262cc439 100644 --- a/src/Sylius/Behat/Context/Api/Shop/ProductReviewContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/ProductReviewContext.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Shop; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; @@ -42,7 +42,7 @@ final class ProductReviewContext implements Context $product = $this->sharedStorage->get('product'); $this->client->index(Resources::PRODUCT_REVIEWS); - $this->client->addFilter('reviewSubject', $this->iriConverter->getIriFromItem($product)); + $this->client->addFilter('reviewSubject', $this->iriConverter->getIriFromResource($product)); $this->client->filter(); } @@ -61,7 +61,7 @@ final class ProductReviewContext implements Context public function iWantToReviewProduct(ProductInterface $product): void { $this->client->buildCreateRequest(Resources::PRODUCT_REVIEWS); - $this->client->addRequestData('product', $this->iriConverter->getIriFromItem($product)); + $this->client->addRequestData('product', $this->iriConverter->getIriFromResource($product)); } /** @@ -103,7 +103,7 @@ final class ProductReviewContext implements Context $product = $this->sharedStorage->get('product'); $this->client->index(Resources::PRODUCT_REVIEWS); - $this->client->addFilter('reviewSubject', $this->iriConverter->getIriFromItem($product)); + $this->client->addFilter('reviewSubject', $this->iriConverter->getIriFromResource($product)); $this->client->addFilter('itemsPerPage', 3); $this->client->addFilter('order[createdAt]', 'desc'); $this->client->filter(); @@ -162,7 +162,7 @@ final class ProductReviewContext implements Context */ public function iShouldBeNotifiedThatIMustCheckReviewRating(): void { - $this->assertViolation('You must check review rating.', 'rating'); + $this->assertError('Request field "rating" should be of type "int".'); } /** @@ -170,7 +170,7 @@ final class ProductReviewContext implements Context */ public function iShouldBeNotifiedThatTitleIsRequired(): void { - $this->assertViolation('Review title should not be blank.', 'title'); + $this->assertError('Request field "title" should be of type "string".'); } /** @@ -194,7 +194,7 @@ final class ProductReviewContext implements Context */ public function iShouldBeNotifiedThatCommentIsRequired(): void { - $this->assertViolation('Review comment should not be blank.', 'comment'); + $this->assertError('Request field "comment" should be of type "string".'); } /** @@ -214,11 +214,11 @@ final class ProductReviewContext implements Context } /** - * @Then I should be notified that rate must be an integer in the range 1-5 + * @Then I should be notified that rating must be between 1 and 5 */ - public function iShouldBeNotifiedThatRateMustBeAnIntegerInTheRange15(): void + public function iShouldBeNotifiedThatRatingMustBeBetween1And5(): void { - $this->assertViolation('Review rating must be an integer in the range 1-5.', 'rating'); + $this->assertViolation('Review rating must be between 1 and 5.', 'rating'); } private function hasReviewsWithTitles(array $titles): bool @@ -239,4 +239,12 @@ final class ProductReviewContext implements Context Assert::same($response->getStatusCode(), 422); Assert::true($this->responseChecker->hasViolationWithMessage($response, $message, $property)); } + + private function assertError(string $error): void + { + $response = $this->client->getLastResponse(); + + Assert::same($response->getStatusCode(), 400); + Assert::same($this->responseChecker->getError($response), $error); + } } diff --git a/src/Sylius/Behat/Context/Api/Shop/ProductVariantContext.php b/src/Sylius/Behat/Context/Api/Shop/ProductVariantContext.php index 14e5546938..079c7e3d68 100644 --- a/src/Sylius/Behat/Context/Api/Shop/ProductVariantContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/ProductVariantContext.php @@ -13,12 +13,15 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Api\Shop; +use ApiPlatform\Api\IriConverterInterface; use Behat\Behat\Context\Context; use Sylius\Behat\Client\ApiClientInterface; use Sylius\Behat\Client\ResponseCheckerInterface; use Sylius\Behat\Context\Api\Resources; use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; +use Sylius\Component\Product\Model\ProductOptionValueInterface; use Webmozart\Assert\Assert; final class ProductVariantContext implements Context @@ -27,6 +30,7 @@ final class ProductVariantContext implements Context private ApiClientInterface $client, private ResponseCheckerInterface $responseChecker, private SharedStorageInterface $sharedStorage, + private IriConverterInterface $iriConverter, ) { } @@ -60,6 +64,27 @@ final class ProductVariantContext implements Context $this->sharedStorage->set('response', $response); } + /** + * @When /^I view variants of the ("[^"]+" product)$/ + */ + public function iViewVariantsOfTheProduct(ProductInterface $product): void + { + $response = $this->client->index(Resources::PRODUCT_VARIANTS, ['product' => $this->iriConverter->getIriFromResource($product)]); + + $this->sharedStorage->set('product_variant_collection', $this->responseChecker->getCollection($response)); + } + + /** + * @When /^I filter (?:them|variants) by ("[^"]+" option value)$/ + */ + public function iFilterVariantsByOption(ProductOptionValueInterface $optionValue): void + { + $this->client->addFilter('optionValues[]', $this->iriConverter->getIriFromResource($optionValue)); + $response = $this->client->filter(); + + $this->sharedStorage->set('product_variant_collection', $this->responseChecker->getCollection($response)); + } + /** * @Then /^(?:the|this) product variant price should be ("[^"]+")$/ * @Then /^I should see the variant price ("[^"]+")$/ @@ -274,6 +299,68 @@ final class ProductVariantContext implements Context } } + /** + * @Then /^I should see variant with ("[^"]+" option) and ("[^"]+" option value) priced at ("[^"]+") at (\d)(?:st|nd|rd|th) position$/ + */ + public function iShouldSeeVariantWithOptionPricedAtAtPosition( + string $expectedOptionName, + string $expectedOptionValueValue, + int $price, + int $position, + ): void { + $variants = $this->sharedStorage->get('product_variant_collection'); + Assert::greaterThan(count($variants), $position - 1, 'There are less variants than expected'); + + $variant = $variants[$position - 1]; + Assert::same($variant['price'], $price); + + foreach ($variant['optionValues'] as $optionValue) { + $optionValueData = $this->fetchItemByIri($optionValue); + $optionData = $this->fetchItemByIri($optionValueData['option']); + + if ($optionData['name'] === $expectedOptionName && $optionValueData['value'] === $expectedOptionValueValue) { + return; + } + } + + throw new \InvalidArgumentException(sprintf( + 'There is no variant with "%s" option and "%s" option value', + $expectedOptionName, + $expectedOptionValueValue, + )); + } + + /** + * @Then /^I should not see variant with "([^"]+)" option "([^"]+)"$/ + */ + public function iShouldNotSeeVariantWithOptionPricedAt(string $expectedOptionName, string $expectedOptionValueValue): void + { + $variants = $this->sharedStorage->get('product_variant_collection'); + + foreach ($variants as $variant) { + foreach ($variant['optionValues'] as $optionValueIri) { + $optionValueData = $this->fetchItemByIri($optionValueIri); + $optionData = $this->fetchItemByIri($optionValueData['option']); + + Assert::false( + $optionData['name'] === $expectedOptionName && + $optionValueData['value'] === $expectedOptionValueValue, + ); + } + } + } + + /** + * @Then I should not see any variants + */ + public function iShouldNotSeeAnyVariants(): void + { + Assert::same( + count($this->sharedStorage->get('product_variant_collection')), + 0, + ); + } + private function findVariant(?ProductVariantInterface $variant): array { $response = $this->sharedStorage->has('response') ? $this->sharedStorage->get('response') : $this->client->getLastResponse(); @@ -286,4 +373,9 @@ final class ProductVariantContext implements Context return $this->responseChecker->getResponseContent($response); } + + private function fetchItemByIri(string $iri): array + { + return $this->responseChecker->getResponseContent($this->client->showByIri($iri)); + } } diff --git a/src/Sylius/Behat/Context/Api/Shop/RegistrationContext.php b/src/Sylius/Behat/Context/Api/Shop/RegistrationContext.php index 3deb8f7877..2be29b9ab4 100644 --- a/src/Sylius/Behat/Context/Api/Shop/RegistrationContext.php +++ b/src/Sylius/Behat/Context/Api/Shop/RegistrationContext.php @@ -251,6 +251,14 @@ final class RegistrationContext implements Context Assert::true($this->responseChecker->getResponseContent($response)['subscribedToNewsletter']); } + /** + * @Then I should be on my account dashboard + * @Then I should be on registration thank you page + */ + public function intentionallyLeftBlank(): void + { + } + private function assertFieldValidationMessage(string $path, string $message): void { $decodedResponse = $this->getResponseContent(); diff --git a/src/Sylius/Behat/Context/Api/Shop/TaxonContext.php b/src/Sylius/Behat/Context/Api/Shop/TaxonContext.php new file mode 100644 index 0000000000..6f059a4d28 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Shop/TaxonContext.php @@ -0,0 +1,77 @@ +objectManager->clear(); // avoiding doctrine cache + $this->client->show(Resources::TAXONS, $taxon->getCode()); + } + + /** + * @Then I should not see :taxon in the vertical menu + */ + public function iShouldNotSeeInTheVerticalMenu(TaxonInterface $taxon): void + { + Assert::false( + $this->isTaxonChildVisible($taxon), + sprintf('Taxon %s is in the vertical menu, but it should not.', $taxon->getName()), + ); + } + + /** + * @Then /^I should see ("([^"]+)" and "([^"]+)" in the vertical menu)$/ + */ + public function iShouldSeeInTheVerticalMenu(iterable $taxons): void + { + foreach ($taxons as $taxon) { + Assert::true( + $this->isTaxonChildVisible($taxon), + sprintf('Taxon %s is not in the vertical menu, but it should be.', $taxon->getName()), + ); + } + } + + private function isTaxonChildVisible(TaxonInterface $taxon): bool + { + $taxonIri = $this->iriConverter->getIriFromResource($taxon); + $response = $this->client->getLastResponse(); + $children = $this->responseChecker->getValue($response, 'children'); + + return in_array($taxonIri, $children, true); + } +} diff --git a/src/Sylius/Behat/Context/Api/Subresources.php b/src/Sylius/Behat/Context/Api/Subresources.php new file mode 100644 index 0000000000..7c6c0743b5 --- /dev/null +++ b/src/Sylius/Behat/Context/Api/Subresources.php @@ -0,0 +1,25 @@ + $adminUserRepository */ + public function __construct( + KernelInterface $kernel, + private UserRepositoryInterface $adminUserRepository, + private UserPasswordHasherInterface $userPasswordHasher, + ) { + $this->application = new Application($kernel); + } + + /** + * @When I want to change password + */ + public function iWantToChangePassword(): void + { + $command = $this->application->find(self::ADMIN_USER_CHANGE_PASSWORD); + + $this->commandTester = new CommandTester($command); + } + + /** + * @When I specify email as :email + */ + public function iSpecifyEmailAs(string $email = ''): void + { + $this->input['email'] = $email; + } + + /** + * @When I specify my new password as :password + */ + public function iSpecifyMyNewPassword(string $password = ''): void + { + $this->input['password'] = $password; + } + + /** + * @When I run command + */ + public function iRunCommand(): void + { + $this->commandTester->setInputs($this->input); + $this->commandTester->execute(['command' => self::ADMIN_USER_CHANGE_PASSWORD]); + } + + /** + * @Then I should be informed that password has been changed successfully + */ + public function iShouldBeInformedThatPasswordHasBeenChangedSuccessfully(): void + { + Assert::contains($this->commandTester->getDisplay(), 'Admin user password has been changed successfully.'); + } + + /** + * @Then I should be able to log in as :email authenticated by :password password + */ + public function iShouldBeAbleToLoginWithEmailAndPassword(string $email = '', string $password = ''): void + { + /** @var AdminUserInterface|null $adminUser */ + $adminUser = $this->adminUserRepository->findOneByEmail($email); + $adminUser->setPlainPassword($password); + + Assert::same($adminUser->getPassword(), $this->userPasswordHasher->hash($adminUser)); + } +} diff --git a/src/Sylius/Behat/Context/Domain/ManagingPriceHistoryContext.php b/src/Sylius/Behat/Context/Domain/ManagingPriceHistoryContext.php new file mode 100644 index 0000000000..2b0adeaf06 --- /dev/null +++ b/src/Sylius/Behat/Context/Domain/ManagingPriceHistoryContext.php @@ -0,0 +1,84 @@ +channelPricingLogEntriesRemover->remove($days); + } + + /** + * @Then /^there should be (\d+) price history entries for (this product)$/ + */ + public function thereShouldBeCountPriceHistoryEntriesForThisProduct(int $count, ProductInterface $product): void + { + $channelPricingLogEntries = $this->channelPricingLogEntryRepository->findBy([ + 'channelPricing' => $this->getChannelPricingFromProduct($product), + ]); + + Assert::count($channelPricingLogEntries, $count); + } + + /** + * @Then /^(this product) should have no entry with original price changed to ("[^"]+")$/ + */ + public function thisProductShouldHaveNoEntryWithOriginalPriceChangedTo( + ProductInterface $product, + int $originalPrice, + ): void { + Assert::null($this->channelPricingLogEntryRepository->findOneBy([ + 'channelPricing' => $this->getChannelPricingFromProduct($product), + 'originalPrice' => $originalPrice, + ])); + } + + /** + * @Then /^(this product)'s price history should be empty$/ + */ + public function thisProductsPriceHistoryShouldBeEmpty(ProductInterface $product): void + { + $this->thereShouldBeCountPriceHistoryEntriesForThisProduct(0, $product); + } + + private function getChannelPricingFromProduct(ProductInterface $product): ChannelPricingInterface + { + $variant = $this->variantResolver->getVariant($product); + Assert::notNull($variant); + + $channelPricing = $variant->getChannelPricings()->first(); + Assert::isInstanceOf($channelPricing, ChannelPricingInterface::class); + + return $channelPricing; + } +} diff --git a/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php index 74dd507bb5..2c3442e601 100644 --- a/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Domain/ManagingPromotionsContext.php @@ -15,6 +15,7 @@ namespace Sylius\Behat\Context\Domain; use Behat\Behat\Context\Context; use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; +use Doctrine\Persistence\ObjectManager; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Promotion\Model\PromotionInterface; use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface; @@ -25,6 +26,7 @@ final class ManagingPromotionsContext implements Context public function __construct( private SharedStorageInterface $sharedStorage, private PromotionRepositoryInterface $promotionRepository, + private ObjectManager $promotionManager, ) { } @@ -48,6 +50,16 @@ final class ManagingPromotionsContext implements Context } } + /** + * @When I archive the :promotion promotion + */ + public function iArchiveThePromotion(PromotionInterface $promotion): void + { + $promotion->setArchivedAt(new \DateTime()); + + $this->promotionManager->flush(); + } + /** * @Then /^(this promotion) should no longer exist in the promotion registry$/ */ @@ -71,4 +83,12 @@ final class ManagingPromotionsContext implements Context { Assert::isInstanceOf($this->sharedStorage->get('last_exception'), ForeignKeyConstraintViolationException::class); } + + /** + * @Then the promotion :promotion should still exist in the registry + */ + public function thePromotionShouldStillExistInTheRegistry(PromotionInterface $promotion): void + { + Assert::notNull($this->promotionRepository->find($promotion)); + } } diff --git a/src/Sylius/Behat/Context/Hook/CacheContext.php b/src/Sylius/Behat/Context/Hook/CacheContext.php new file mode 100644 index 0000000000..2c28aad8f1 --- /dev/null +++ b/src/Sylius/Behat/Context/Hook/CacheContext.php @@ -0,0 +1,32 @@ +cache->clear(); + } +} diff --git a/src/Sylius/Behat/Context/Setup/CartContext.php b/src/Sylius/Behat/Context/Setup/CartContext.php index 0e628ec8ac..81f7bdfd8e 100644 --- a/src/Sylius/Behat/Context/Setup/CartContext.php +++ b/src/Sylius/Behat/Context/Setup/CartContext.php @@ -20,9 +20,11 @@ use Sylius\Bundle\ApiBundle\Command\Cart\PickupCart; use Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; +use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Model\ShopUserInterface; +use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Sylius\Component\Product\Model\ProductOptionInterface; use Sylius\Component\Product\Model\ProductOptionValueInterface; use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; @@ -31,7 +33,11 @@ use Symfony\Component\Messenger\MessageBusInterface; final class CartContext implements Context { + /** + * @param OrderRepositoryInterface $orderRepository + */ public function __construct( + private OrderRepositoryInterface $orderRepository, private MessageBusInterface $commandBus, private ProductVariantResolverInterface $productVariantResolver, private RandomnessGeneratorInterface $generator, @@ -48,7 +54,7 @@ final class CartContext implements Context } /** - * @Given /^I have(?:| added) (\d+) (products "[^"]+") (?:to|in) the (cart)$/ + * @Given /^I have(?:| added) (\d+) (product(?:|s) "[^"]+") (?:to|in) the (cart)$/ */ public function iHaveAddedProductsToTheCart(int $quantity, ProductInterface $product, ?string $tokenValue): void { @@ -60,6 +66,7 @@ final class CartContext implements Context * @Given /^I (?:have|had) (product "[^"]+") in the (cart)$/ * @Given /^I have (product "[^"]+") added to the (cart)$/ * @Given /^the (?:customer|visitor) has (product "[^"]+") in the (cart)$/ + * @Given /^the (?:customer|visitor) added ("[^"]+" product) to the (cart)$/ * @When /^the (?:customer|visitor) try to add (product "[^"]+") in the customer (cart)$/ */ public function iAddedProductToTheCart(ProductInterface $product, ?string $tokenValue): void @@ -176,7 +183,7 @@ final class CartContext implements Context private function addProductToCart(ProductInterface $product, ?string $tokenValue, int $quantity = 1): void { - if ($tokenValue === null) { + if ($tokenValue === null || !$this->doesCartWithTokenExist($tokenValue)) { $tokenValue = $this->pickupCart(); } @@ -188,4 +195,9 @@ final class CartContext implements Context $this->sharedStorage->set('product', $product); } + + private function doesCartWithTokenExist(string $tokenValue): bool + { + return $this->orderRepository->findCartByTokenValue($tokenValue) !== null; + } } diff --git a/src/Sylius/Behat/Context/Setup/CatalogPromotionContext.php b/src/Sylius/Behat/Context/Setup/CatalogPromotionContext.php index a66ff1c7a5..00aaf11d24 100644 --- a/src/Sylius/Behat/Context/Setup/CatalogPromotionContext.php +++ b/src/Sylius/Behat/Context/Setup/CatalogPromotionContext.php @@ -15,7 +15,7 @@ namespace Sylius\Behat\Context\Setup; use Behat\Behat\Context\Context; use Doctrine\ORM\EntityManagerInterface; -use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Bundle\CoreBundle\CatalogPromotion\Calculator\FixedDiscountPriceCalculator; use Sylius\Bundle\CoreBundle\CatalogPromotion\Calculator\PercentageDiscountPriceCalculator; @@ -47,7 +47,7 @@ final class CatalogPromotionContext implements Context private FactoryInterface $catalogPromotionActionFactory, private EntityManagerInterface $entityManager, private ChannelRepositoryInterface $channelRepository, - private StateMachineFactoryInterface $stateMachineFactory, + private StateMachineInterface $stateMachine, private MessageBusInterface $eventBus, private SharedStorageInterface $sharedStorage, ) { @@ -755,9 +755,8 @@ final class CatalogPromotionContext implements Context return; } - $stateMachine = $this->stateMachineFactory->get($catalogPromotion, CatalogPromotionTransitions::GRAPH); - $stateMachine->apply(CatalogPromotionTransitions::TRANSITION_PROCESS); - $stateMachine->apply(CatalogPromotionTransitions::TRANSITION_ACTIVATE); + $this->stateMachine->apply($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_PROCESS); + $this->stateMachine->apply($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_ACTIVATE); $this->entityManager->flush(); } @@ -767,12 +766,89 @@ final class CatalogPromotionContext implements Context */ public function theCatalogPromotionIsCurrentlyBeingProcessed(CatalogPromotionInterface $catalogPromotion): void { - $stateMachine = $this->stateMachineFactory->get($catalogPromotion, CatalogPromotionTransitions::GRAPH); - $stateMachine->apply(CatalogPromotionTransitions::TRANSITION_PROCESS); + $this->stateMachine->apply($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_PROCESS); $this->entityManager->flush(); } + /** + * @Given the :catalogPromotion catalog promotion is enabled + */ + public function theCatalogPromotionIsEnabled(CatalogPromotionInterface $catalogPromotion): void + { + $catalogPromotion->setEnabled(true); + $this->entityManager->flush(); + + $this->eventBus->dispatch(new CatalogPromotionUpdated($catalogPromotion->getCode())); + } + + /** + * @Given there is disabled catalog promotion named :name + */ + public function thereIsCatalogPromotionsNamed(string $name): void + { + $this->createCatalogPromotion(name: $name, enabled: false); + + $this->entityManager->flush(); + } + + /** + * @Given /^there is a catalog promotion "([^"]+)" with priority ([^"]+) that reduces price by ("[^"]+") and applies on ("[^"]+" product)$/ + */ + public function thereIsACatalogPromotionWithPriorityThatReducesPriceByAndAppliesOnProduct( + string $name, + int $priority, + float $discount, + ProductInterface $product, + ): void { + $catalogPromotion = $this->createCatalogPromotion( + name: $name, + scopes: [[ + 'type' => InForProductScopeVariantChecker::TYPE, + 'configuration' => ['products' => [$product->getCode()]], + ]], + actions: [[ + 'type' => PercentageDiscountPriceCalculator::TYPE, + 'configuration' => ['amount' => $discount], + ]], + priority: $priority, + ); + + $this->entityManager->flush(); + + $this->eventBus->dispatch(new CatalogPromotionUpdated($catalogPromotion->getCode())); + } + + /** + * @Given /^there is disabled catalog promotion "([^"]+)" with priority ([^"]+) that reduces price by fixed ("[^"]+") in the ("[^"]+" channel) and applies on ("[^"]+" product)$/ + */ + public function thereIsDisabledCatalogPromotionWithPriorityThatReducesPriceByFixedInTheChannelAndAppliesOnProduct( + string $name, + int $priority, + int $discount, + ChannelInterface $channel, + ProductInterface $product, + ): void { + $catalogPromotion = $this->createCatalogPromotion( + name: $name, + channels: [$channel], + scopes: [[ + 'type' => InForProductScopeVariantChecker::TYPE, + 'configuration' => ['products' => [$product->getCode()]], + ]], + actions: [[ + 'type' => FixedDiscountPriceCalculator::TYPE, + 'configuration' => [$channel->getCode() => ['amount' => $discount / 100]], + ]], + priority: $priority, + enabled: false, + ); + + $this->entityManager->flush(); + + $this->eventBus->dispatch(new CatalogPromotionUpdated($catalogPromotion->getCode())); + } + private function createCatalogPromotion( string $name, ?string $code = null, diff --git a/src/Sylius/Behat/Context/Setup/ChannelContext.php b/src/Sylius/Behat/Context/Setup/ChannelContext.php index 84f02324f6..5cbb00903f 100644 --- a/src/Sylius/Behat/Context/Setup/ChannelContext.php +++ b/src/Sylius/Behat/Context/Setup/ChannelContext.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Setup; use Behat\Behat\Context\Context; +use Behat\Step\Given; use Doctrine\Persistence\ObjectManager; use Sylius\Behat\Service\Setter\ChannelContextSetterInterface; use Sylius\Behat\Service\SharedStorageInterface; @@ -26,10 +27,12 @@ use Sylius\Component\Core\Model\ShopBillingData; use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Core\Test\Services\DefaultChannelFactoryInterface; use Sylius\Component\Locale\Model\LocaleInterface; -use Sylius\Component\Resource\Repository\RepositoryInterface; final class ChannelContext implements Context { + /** + * @param ChannelRepositoryInterface $channelRepository + */ public function __construct( private SharedStorageInterface $sharedStorage, private ChannelContextSetterInterface $channelContextSetter, @@ -37,7 +40,6 @@ final class ChannelContext implements Context private DefaultChannelFactoryInterface $defaultChannelFactory, private ChannelRepositoryInterface $channelRepository, private ObjectManager $channelManager, - private RepositoryInterface $localeRepository, ) { } @@ -51,10 +53,25 @@ final class ChannelContext implements Context $this->channelManager->flush(); } + /** + * @Given /^the (channel "[^"]+") has ("([^"]+)" and "([^"]+)" taxons) excluded from showing the lowest price of discounted products$/ + */ + public function theTaxonAndTaxonAreExcludedFromShowingTheLowestPriceOfDiscountedProductsOnThisChannel( + ChannelInterface $channel, + iterable $taxons, + ): void { + /** @var TaxonInterface $taxon */ + foreach ($taxons as $taxon) { + $channel->getChannelPriceHistoryConfig()->addTaxonExcludedFromShowingLowestPrice($taxon); + } + + $this->channelManager->flush(); + } + /** * @Given the store operates on a single channel in "United States" */ - public function storeOperatesOnASingleChannelInUnitedStates() + public function storeOperatesOnASingleChannelInUnitedStates(): void { $defaultData = $this->unitedStatesChannelFactory->create(); @@ -63,9 +80,9 @@ final class ChannelContext implements Context } /** - * @Given the store operates on a single channel in the "United States" named :channelIdentifier + * @Given the store operates on a single channel in the "United States" named :channelName */ - public function storeOperatesOnASingleChannelInTheUnitedStatesNamed(string $channelName) + public function storeOperatesOnASingleChannelInTheUnitedStatesNamed(string $channelName): void { $channelCode = StringInflector::nameToLowercaseCode($channelName); $defaultData = $this->unitedStatesChannelFactory->create($channelCode, $channelName); @@ -78,7 +95,7 @@ final class ChannelContext implements Context * @Given the store operates on a single channel * @Given the store operates on a single channel in :currencyCode currency */ - public function storeOperatesOnASingleChannel($currencyCode = null) + public function storeOperatesOnASingleChannel(?string $currencyCode = null): void { $defaultData = $this->defaultChannelFactory->create(null, null, $currencyCode); @@ -122,7 +139,7 @@ final class ChannelContext implements Context /** * @Given the channel :channel is enabled */ - public function theChannelIsEnabled(ChannelInterface $channel) + public function theChannelIsEnabled(ChannelInterface $channel): void { $this->changeChannelState($channel, true); } @@ -131,15 +148,25 @@ final class ChannelContext implements Context * @Given the channel :channel is disabled * @Given the channel :channel has been disabled */ - public function theChannelIsDisabled(ChannelInterface $channel) + public function theChannelIsDisabled(ChannelInterface $channel): void { $this->changeChannelState($channel, false); } + /** + * @Given /^the (channel "[^"]+") has showing the lowest price of discounted products (enabled|disabled)$/ + */ + public function theChannelHasShowingTheLowestPriceOfDiscountedProducts(ChannelInterface $channel, string $visible): void + { + $channel->getChannelPriceHistoryConfig()->setLowestPriceForDiscountedProductsVisible($visible === 'enabled'); + + $this->channelManager->flush(); + } + /** * @Given channel :channel has been deleted */ - public function iChannelHasBeenDeleted(ChannelInterface $channel) + public function iChannelHasBeenDeleted(ChannelInterface $channel): void { $this->channelRepository->remove($channel); } @@ -147,7 +174,7 @@ final class ChannelContext implements Context /** * @Given /^(its) default tax zone is (zone "([^"]+)")$/ */ - public function itsDefaultTaxRateIs(ChannelInterface $channel, ZoneInterface $defaultTaxZone) + public function itsDefaultTaxRateIs(ChannelInterface $channel, ZoneInterface $defaultTaxZone): void { $channel->setDefaultTaxZone($defaultTaxZone); $this->channelManager->flush(); @@ -157,7 +184,7 @@ final class ChannelContext implements Context * @Given /^(this channel) has contact email set as "([^"]+)"$/ * @Given /^(this channel) has no contact email set$/ */ - public function thisChannelHasContactEmailSetAs(ChannelInterface $channel, $contactEmail = null) + public function thisChannelHasContactEmailSetAs(ChannelInterface $channel, ?string $contactEmail = null): void { $channel->setContactEmail($contactEmail); $this->channelManager->flush(); @@ -166,7 +193,7 @@ final class ChannelContext implements Context /** * @Given /^on (this channel) shipping step is skipped if only a single shipping method is available$/ */ - public function onThisChannelShippingStepIsSkippedIfOnlyASingleShippingMethodIsAvailable(ChannelInterface $channel) + public function onThisChannelShippingStepIsSkippedIfOnlyASingleShippingMethodIsAvailable(ChannelInterface $channel): void { $channel->setSkippingShippingStepAllowed(true); @@ -178,7 +205,7 @@ final class ChannelContext implements Context */ public function onThisChannelPaymentStepIsSkippedIfOnlyASinglePaymentMethodIsAvailable( ChannelInterface $channel, - ) { + ): void { $channel->setSkippingPaymentStepAllowed(true); $this->channelManager->flush(); @@ -187,13 +214,23 @@ final class ChannelContext implements Context /** * @Given /^on (this channel) account verification is not required$/ */ - public function onThisChannelAccountVerificationIsNotRequired(ChannelInterface $channel) + public function onThisChannelAccountVerificationIsNotRequired(ChannelInterface $channel): void { $channel->setAccountVerificationRequired(false); $this->channelManager->flush(); } + /** + * @Given /^on (this channel) account verification is required$/ + */ + public function onThisChannelAccountVerificationIsRequired(ChannelInterface $channel): void + { + $channel->setAccountVerificationRequired(true); + + $this->channelManager->flush(); + } + /** * @Given channel :channel billing data is :company, :street, :postcode :city, :country with :taxId tax ID */ @@ -277,6 +314,38 @@ final class ChannelContext implements Context $this->channelManager->flush(); } + /** + * @Given /^(this channel) has (\d+) day(?:|s) set as the lowest price for discounted products checking period$/ + */ + public function thisChannelHasDaysSetAsTheLowestPriceForDiscountedProductsCheckingPeriod( + ChannelInterface $channel, + int $days, + ): void { + $channel->getChannelPriceHistoryConfig()->setLowestPriceForDiscountedProductsCheckingPeriod($days); + + $this->channelManager->flush(); + } + + /** + * @Given the :taxon taxon is excluded from showing the lowest price of discounted products in the :channel channel + */ + public function theTaxonIsExcludedFromShowingTheLowestPriceOfDiscountedProductsInTheChannel( + TaxonInterface $taxon, + ChannelInterface $channel, + ): void { + $channel->getChannelPriceHistoryConfig()->addTaxonExcludedFromShowingLowestPrice($taxon); + + $this->channelManager->flush(); + } + + /** + * @Given /^the lowest price of discounted products prior to the current discount is disabled on (this channel)$/ + */ + public function theLowestPriceOfDiscountedProductsPriorToTheCurrentDiscountIsDisabledOnThisChannel(ChannelInterface $channel): void + { + $channel->getChannelPriceHistoryConfig()->setLowestPriceForDiscountedProductsVisible(false); + } + /** * @Given the store also operates in :locale locale */ @@ -301,10 +370,7 @@ final class ChannelContext implements Context $this->channelManager->flush(); } - /** - * @param bool $state - */ - private function changeChannelState(ChannelInterface $channel, $state) + private function changeChannelState(ChannelInterface $channel, bool $state): void { $channel->setEnabled($state); $this->channelManager->flush(); diff --git a/src/Sylius/Behat/Context/Setup/CustomerContext.php b/src/Sylius/Behat/Context/Setup/CustomerContext.php index 33c5d0217e..f06d2af28d 100644 --- a/src/Sylius/Behat/Context/Setup/CustomerContext.php +++ b/src/Sylius/Behat/Context/Setup/CustomerContext.php @@ -139,6 +139,7 @@ final class CustomerContext implements Context /** * @Given /^(the customer) belongs to (group "([^"]+)")$/ + * @Given /^(this customer) belongs to (group "([^"]+)")$/ */ public function theCustomerBelongsToGroup(CustomerInterface $customer, CustomerGroupInterface $customerGroup) { diff --git a/src/Sylius/Behat/Context/Setup/OrderContext.php b/src/Sylius/Behat/Context/Setup/OrderContext.php index 29c0d4a291..a01d084b91 100644 --- a/src/Sylius/Behat/Context/Setup/OrderContext.php +++ b/src/Sylius/Behat/Context/Setup/OrderContext.php @@ -15,11 +15,14 @@ namespace Sylius\Behat\Context\Setup; use Behat\Behat\Context\Context; use Doctrine\Persistence\ObjectManager; -use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Calendar\Provider\DateTimeProviderInterface; +use Sylius\Component\Addressing\Model\CountryInterface; use Sylius\Component\Core\Model\AddressInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ChannelPricingInterface; +use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\OrderItemInterface; use Sylius\Component\Core\Model\ProductInterface; @@ -31,8 +34,9 @@ use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Core\OrderCheckoutTransitions; use Sylius\Component\Core\OrderPaymentTransitions; use Sylius\Component\Core\OrderShippingTransitions; +use Sylius\Component\Core\Repository\CustomerRepositoryInterface; use Sylius\Component\Core\Repository\OrderRepositoryInterface; -use Sylius\Component\Customer\Model\CustomerInterface; +use Sylius\Component\Order\Model\OrderInterface as BaseOrderInterface; use Sylius\Component\Order\Modifier\OrderItemQuantityModifierInterface; use Sylius\Component\Order\OrderTransitions; use Sylius\Component\Payment\Model\PaymentInterface; @@ -41,6 +45,7 @@ use Sylius\Component\Payment\PaymentTransitions; use Sylius\Component\Payment\Repository\PaymentMethodRepositoryInterface; use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; use Sylius\Component\Resource\Factory\FactoryInterface; +use Sylius\Component\Resource\Generator\RandomnessGeneratorInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Component\Shipping\Repository\ShippingMethodRepositoryInterface; use Sylius\Component\Shipping\ShipmentTransitions; @@ -48,22 +53,36 @@ use Webmozart\Assert\Assert; final class OrderContext implements Context { + /** + * @param FactoryInterface $orderFactory + * @param FactoryInterface $addressFactory + * @param FactoryInterface $customerFactory + * @param FactoryInterface $orderItemFactory + * @param FactoryInterface $shipmentFactory + * @param RepositoryInterface $countryRepository + * @param CustomerRepositoryInterface $customerRepository + * @param OrderRepositoryInterface $orderRepository + * @param PaymentMethodRepositoryInterface $paymentMethodRepository + * @param ShippingMethodRepositoryInterface $shippingMethodRepository + */ public function __construct( - private SharedStorageInterface $sharedStorage, - private FactoryInterface $orderFactory, - private FactoryInterface $addressFactory, - private FactoryInterface $customerFactory, - private FactoryInterface $orderItemFactory, - private FactoryInterface $shipmentFactory, - private StateMachineFactoryInterface $stateMachineFactory, - private RepositoryInterface $countryRepository, - private RepositoryInterface $customerRepository, - private OrderRepositoryInterface $orderRepository, - private PaymentMethodRepositoryInterface $paymentMethodRepository, - private ShippingMethodRepositoryInterface $shippingMethodRepository, - private ProductVariantResolverInterface $variantResolver, - private OrderItemQuantityModifierInterface $itemQuantityModifier, - private ObjectManager $objectManager, + private readonly SharedStorageInterface $sharedStorage, + private readonly FactoryInterface $orderFactory, + private readonly FactoryInterface $addressFactory, + private readonly FactoryInterface $customerFactory, + private readonly FactoryInterface $orderItemFactory, + private readonly FactoryInterface $shipmentFactory, + private readonly StateMachineInterface $stateMachine, + private readonly RepositoryInterface $countryRepository, + private readonly RepositoryInterface $customerRepository, + private readonly OrderRepositoryInterface $orderRepository, + private readonly PaymentMethodRepositoryInterface $paymentMethodRepository, + private readonly ShippingMethodRepositoryInterface $shippingMethodRepository, + private readonly ProductVariantResolverInterface $variantResolver, + private readonly OrderItemQuantityModifierInterface $itemQuantityModifier, + private readonly ObjectManager $objectManager, + private readonly DateTimeProviderInterface $dateTimeProvider, + private readonly RandomnessGeneratorInterface $randomnessGenerator, ) { } @@ -77,8 +96,8 @@ final class OrderContext implements Context */ public function thereIsCustomerThatPlacedOrder( CustomerInterface $customer, - string $orderNumber = null, - ChannelInterface $channel = null, + ?string $orderNumber = null, + ?ChannelInterface $channel = null, ): void { $order = $this->createOrder($customer, $orderNumber, $channel); @@ -106,7 +125,7 @@ final class OrderContext implements Context AddressInterface $address, ShippingMethodInterface $shippingMethod, PaymentMethodInterface $paymentMethod, - ) { + ): void { $this->placeOrder($product, $shippingMethod, $address, $paymentMethod, $customer, 1); $this->objectManager->flush(); } @@ -120,7 +139,7 @@ final class OrderContext implements Context AddressInterface $address, ShippingMethodInterface $shippingMethod, PaymentMethodInterface $paymentMethod, - ) { + ): void { $customer = $this->createCustomer($email); $this->customerRepository->add($customer); @@ -138,7 +157,7 @@ final class OrderContext implements Context AddressInterface $address, ShippingMethodInterface $shippingMethod, PaymentMethodInterface $paymentMethod, - ) { + ): void { $customer = $this->createCustomer($email); $this->customerRepository->add($customer); @@ -152,7 +171,7 @@ final class OrderContext implements Context /** * @Given a customer :customer added something to cart */ - public function customerStartedCheckout(CustomerInterface $customer) + public function customerStartedCheckout(CustomerInterface $customer): void { $cart = $this->createCart($customer); @@ -167,10 +186,12 @@ final class OrderContext implements Context public function theCustomerAddedProductToTheCart(CustomerInterface $customer, ProductInterface $product): void { $cart = $this->createCart($customer); + $variant = $this->getProductVariant($product); + $this->addProductVariantsToOrderWithChannelPrice( $cart, $this->sharedStorage->get('channel'), - $this->variantResolver->getVariant($product), + $variant, 1, ); @@ -182,8 +203,9 @@ final class OrderContext implements Context /** * @Given /^(I) placed (an order "[^"]+")$/ */ - public function iPlacedAnOrder(ShopUserInterface $user, $orderNumber) + public function iPlacedAnOrder(ShopUserInterface $user, string $orderNumber): void { + /** @var CustomerInterface $customer */ $customer = $user->getCustomer(); $order = $this->createOrder($customer, $orderNumber); @@ -196,7 +218,7 @@ final class OrderContext implements Context * @Given /^the customer ("[^"]+" addressed it to "[^"]+", "[^"]+" "[^"]+" in the "[^"]+"(?:|, "[^"]+"))$/ * @Given /^I (addressed it to "[^"]+", "[^"]+", "[^"]+" "[^"]+" in the "[^"]+"(?:|, "[^"]+"))$/ */ - public function theCustomerAddressedItTo(AddressInterface $address) + public function theCustomerAddressedItTo(AddressInterface $address): void { /** @var OrderInterface $order */ $order = $this->sharedStorage->get('order'); @@ -208,7 +230,7 @@ final class OrderContext implements Context /** * @Given the customer changed shipping address' street to :street */ - public function theCustomerChangedShippingAddressStreetTo($street) + public function theCustomerChangedShippingAddressStreetTo(string $street): void { /** @var OrderInterface $order */ $order = $this->sharedStorage->get('order'); @@ -226,7 +248,7 @@ final class OrderContext implements Context * @Given /^for the billing address (of "[^"]+" in the "[^"]+", "[^"]+" "[^"]+", "[^"]+")$/ * @Given /^for the billing address (of "[^"]+" in the "[^"]+", "[^"]+" "([^"]+)", "[^"]+", "[^"]+")$/ */ - public function forTheBillingAddressOf(AddressInterface $address) + public function forTheBillingAddressOf(AddressInterface $address): void { /** @var OrderInterface $order */ $order = $this->sharedStorage->get('order'); @@ -242,7 +264,7 @@ final class OrderContext implements Context * @Given /^the customer ("[^"]+" addressed it to "[^"]+", "[^"]+" "[^"]+" in the "[^"]+") with identical billing address$/ * @Given /^I (addressed it to "[^"]+", "[^"]+", "[^"]+" "[^"]+" in the "[^"]+") with identical billing address$/ */ - public function theCustomerAddressedItToWithIdenticalBillingAddress(AddressInterface $address) + public function theCustomerAddressedItToWithIdenticalBillingAddress(AddressInterface $address): void { $this->theCustomerAddressedItTo($address); $this->forTheBillingAddressOf(clone $address); @@ -256,7 +278,7 @@ final class OrderContext implements Context ShippingMethodInterface $shippingMethod, AddressInterface $address, PaymentMethodInterface $paymentMethod, - ) { + ): void { /** @var OrderInterface $order */ $order = $this->sharedStorage->get('order'); @@ -293,7 +315,7 @@ final class OrderContext implements Context public function theCustomerChoseShippingWithPayment( ShippingMethodInterface $shippingMethod, PaymentMethodInterface $paymentMethod, - ) { + ): void { /** @var OrderInterface $order */ $order = $this->sharedStorage->get('order'); @@ -306,7 +328,7 @@ final class OrderContext implements Context /** * @Given /^the customer chose ("[^"]+" shipping method)$/ */ - public function theCustomerChoseShippingMethod(ShippingMethodInterface $shippingMethod) + public function theCustomerChoseShippingMethod(ShippingMethodInterface $shippingMethod): void { /** @var OrderInterface $order */ $order = $this->sharedStorage->get('order'); @@ -318,7 +340,7 @@ final class OrderContext implements Context $this->applyTransitionOnOrderCheckout($order, OrderCheckoutTransitions::TRANSITION_SELECT_SHIPPING); $this->applyTransitionOnOrderCheckout($order, OrderCheckoutTransitions::TRANSITION_COMPLETE); if (!$order->getPayments()->isEmpty()) { - $this->stateMachineFactory->get($order, OrderPaymentTransitions::GRAPH)->apply(OrderPaymentTransitions::TRANSITION_PAY); + $this->stateMachine->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_PAY); } $this->objectManager->flush(); @@ -327,7 +349,7 @@ final class OrderContext implements Context /** * @Given /^the customer chose ("[^"]+" payment)$/ */ - public function theCustomerChosePayment(PaymentMethodInterface $paymentMethod) + public function theCustomerChosePayment(PaymentMethodInterface $paymentMethod): void { /** @var OrderInterface $order */ $order = $this->sharedStorage->get('order'); @@ -346,9 +368,11 @@ final class OrderContext implements Context * @Given the customer bought a single :product * @Given I bought a single :product */ - public function theCustomerBoughtSingleProduct(ProductInterface $product, ?ChannelInterface $channel = null) + public function theCustomerBoughtSingleProduct(ProductInterface $product, ?ChannelInterface $channel = null): void { - $this->addProductVariantToOrder($this->variantResolver->getVariant($product), 1, $channel); + $variant = $this->getProductVariant($product); + + $this->addProductVariantToOrder($variant, 1, $channel); $this->objectManager->flush(); } @@ -360,7 +384,9 @@ final class OrderContext implements Context ProductInterface $product, ShippingMethodInterface $shippingMethod, ): void { - $this->addProductVariantToOrder($this->variantResolver->getVariant($product), 1); + $variant = $this->getProductVariant($product); + + $this->addProductVariantToOrder($variant, 1); /** @var OrderInterface $order */ $order = $this->sharedStorage->get('order'); @@ -378,7 +404,7 @@ final class OrderContext implements Context * @Given /^the customer bought ((?:a|an) "[^"]+") and ((?:a|an) "[^"]+")$/ * @Given /^I bought ((?:a|an) "[^"]+") and ((?:a|an) "[^"]+")$/ */ - public function theCustomerBoughtProductAndProduct(ProductInterface $product, ProductInterface $secondProduct) + public function theCustomerBoughtProductAndProduct(ProductInterface $product, ProductInterface $secondProduct): void { $this->theCustomerBoughtSingleProduct($product); $this->theCustomerBoughtSingleProduct($secondProduct); @@ -387,9 +413,10 @@ final class OrderContext implements Context /** * @Given /^the customer bought (\d+) ("[^"]+" products)$/ */ - public function theCustomerBoughtSeveralProducts($quantity, ProductInterface $product) + public function theCustomerBoughtSeveralProducts(int $quantity, ProductInterface $product): void { - $variant = $this->variantResolver->getVariant($product); + $variant = $this->getProductVariant($product); + $this->addProductVariantToOrder($variant, $quantity); $this->objectManager->flush(); @@ -398,7 +425,7 @@ final class OrderContext implements Context /** * @Given /^the customer bought ([^"]+) units of ("[^"]+" variant of product "[^"]+")$/ */ - public function theCustomerBoughtSeveralVariantsOfProduct($quantity, ProductVariantInterface $variant) + public function theCustomerBoughtSeveralVariantsOfProduct(int $quantity, ProductVariantInterface $variant): void { $this->addProductVariantToOrder($variant, $quantity); @@ -409,7 +436,7 @@ final class OrderContext implements Context * @Given /^the customer bought a single ("[^"]+" variant of product "[^"]+")$/ * @Given /^the customer also bought a ("[^"]+" variant of product "[^"]+")$/ */ - public function theCustomerBoughtSingleProductVariant(ProductVariantInterface $productVariant) + public function theCustomerBoughtSingleProductVariant(ProductVariantInterface $productVariant): void { $this->addProductVariantToOrder($productVariant); @@ -420,9 +447,11 @@ final class OrderContext implements Context * @Given the customer bought a single :product using :coupon coupon * @Given I bought a single :product using :coupon coupon */ - public function theCustomerBoughtSingleUsing(ProductInterface $product, PromotionCouponInterface $coupon) + public function theCustomerBoughtSingleUsing(ProductInterface $product, PromotionCouponInterface $coupon): void { - $order = $this->addProductVariantToOrder($this->variantResolver->getVariant($product)); + $variant = $this->getProductVariant($product); + + $order = $this->addProductVariantToOrder($variant); $order->setPromotionCoupon($coupon); $this->objectManager->flush(); @@ -431,7 +460,7 @@ final class OrderContext implements Context /** * @Given I used :coupon coupon */ - public function iUsedCoupon(PromotionCouponInterface $coupon) + public function iUsedCoupon(PromotionCouponInterface $coupon): void { $order = $this->sharedStorage->get('order'); $order->setPromotionCoupon($coupon); @@ -444,12 +473,13 @@ final class OrderContext implements Context */ public function iHaveAlreadyPlacedOrderNthTimes( ShopUserInterface $user, - $numberOfOrders, + int $numberOfOrders, ProductInterface $product, ShippingMethodInterface $shippingMethod, AddressInterface $address, PaymentMethodInterface $paymentMethod, - ) { + ): void { + /** @var CustomerInterface $customer */ $customer = $user->getCustomer(); for ($i = 0; $i < $numberOfOrders; ++$i) { $this->placeOrder($product, $shippingMethod, $address, $paymentMethod, $customer, $i); @@ -467,7 +497,7 @@ final class OrderContext implements Context public function thereIsAOrderWithProduct( string $orderNumber, ProductInterface $product, - string $state = null, + ?string $state = null, ?ChannelInterface $channel = null, ): void { $order = $this->createOrder($this->createOrProvideCustomer('amba@fatima.org'), $orderNumber, $channel); @@ -498,12 +528,14 @@ final class OrderContext implements Context /** * @Given /^(this customer) has(?:| also) placed (an order "[^"]+") at "([^"]+)"$/ + * + * @throws \Exception */ - public function thisCustomerHasPlacedAnOrderAtDate(CustomerInterface $customer, $number, $checkoutCompletedAt) + public function thisCustomerHasPlacedAnOrderAtDate(CustomerInterface $customer, string $number, string $checkoutCompletedAt): void { $order = $this->createOrder($customer, $number); $order->setCheckoutCompletedAt(new \DateTime($checkoutCompletedAt)); - $order->setState(OrderInterface::STATE_NEW); + $order->setState(BaseOrderInterface::STATE_NEW); $this->orderRepository->add($order); } @@ -511,10 +543,10 @@ final class OrderContext implements Context /** * @Given /^(this customer) has(?:| also) placed (an order "[^"]+") on a (channel "[^"]+")$/ */ - public function thisCustomerHasPlacedAnOrderOnAChannel(CustomerInterface $customer, $number, $channel) + public function thisCustomerHasPlacedAnOrderOnAChannel(CustomerInterface $customer, string $number, ChannelInterface $channel): void { $order = $this->createOrder($customer, $number, $channel); - $order->setState(OrderInterface::STATE_NEW); + $order->setState(BaseOrderInterface::STATE_NEW); $this->orderRepository->add($order); $this->sharedStorage->set('order', $order); @@ -523,7 +555,7 @@ final class OrderContext implements Context /** * @Given /^(this customer) has(?:| also) started checkout on a (channel "[^"]+")$/ */ - public function thisCustomerHasStartedCheckoutOnAChannel(CustomerInterface $customer, $channel) + public function thisCustomerHasStartedCheckoutOnAChannel(CustomerInterface $customer, ChannelInterface $channel): void { $order = $this->createOrder($customer, null, $channel); @@ -558,14 +590,13 @@ final class OrderContext implements Context } /** - * @Given :numberOfCustomers customers have added products to the cart for total of :total + * @Given /^(\d+) new customers have added products to the cart for total of ("[^"]+")$/ */ - public function customersHaveAddedProductsToTheCartForTotalOf($numberOfCustomers, $total) + public function customersHaveAddedProductsToTheCartForTotalOf(int $numberOfCustomers, int $total): void { $customers = $this->generateCustomers($numberOfCustomers); $sampleProductVariant = $this->sharedStorage->get('variant'); - $total = $this->getPriceFromString($total); for ($i = 0; $i < $numberOfCustomers; ++$i) { $order = $this->createCart($customers[random_int(0, $numberOfCustomers - 1)]); @@ -582,62 +613,63 @@ final class OrderContext implements Context } /** - * @Given a single customer has placed an order for total of :total - * @Given :numberOfCustomers customers have placed :numberOfOrders orders for total of :total - * @Given then :numberOfCustomers more customers have placed :numberOfOrders orders for total of :total + * @Given /^a single customer has placed an order for total of ("[^"]+")$/ */ - public function customersHavePlacedOrdersForTotalOf( - string $total, - int $numberOfCustomers = 1, - int $numberOfOrders = 1, - ): void { + public function aSingleCustomerHasPlacedAnOrderForTotalOf(int $total): void + { + $this->createOrders(numberOfCustomers: 1, numberOfOrders: 1, total: $total); + } + + /** + * @Given /^(\d+) (?:|more )new customers have placed (\d+) orders for total of ("[^"]+")$/ + */ + public function customersHavePlacedOrdersForTotalOf(int $numberOfCustomers, int $numberOfOrders, int $total): void + { $this->createOrders($numberOfCustomers, $numberOfOrders, $total); } /** - * @Given :numberOfCustomers customers have fulfilled :numberOfOrders orders placed for total of :total - * @Given then :numberOfCustomers more customers have fulfilled :numberOfOrders orders placed for total of :total + * @Given /^(\d+) new customers have fulfilled (\d+) orders placed for total of ("[^"]+")$/ */ public function customersHaveFulfilledOrdersPlacedForTotalOf( int $numberOfCustomers, int $numberOfOrders, - string $total, + int $total, ): void { $this->createOrders($numberOfCustomers, $numberOfOrders, $total, true); } /** - * @Given :numberOfCustomers customers have placed :numberOfOrders orders for total of :total mostly :product product - * @Given then :numberOfCustomers more customers have placed :numberOfOrders orders for total of :total mostly :product product + * @Given /^(\d+) (?:|more )new customers have placed (\d+) orders for total of ("[^"]+") mostly ("[^"]+" product)$/ */ public function customersHavePlacedOrdersForTotalOfMostlyProduct( int $numberOfCustomers, int $numberOfOrders, - string $total, + int $total, ProductInterface $product, ): void { $this->createOrdersWithProduct($numberOfCustomers, $numberOfOrders, $total, $product); } /** - * @Given (then) :numberOfCustomers (more) customers have fulfilled :numberOfOrders orders placed for total of :total mostly :product product + * @Given /^(\d+) (?:|more )new customers have fulfilled (\d+) orders placed for total of ("[^"]+") mostly ("[^"]+" product)$/ */ public function customersHaveFulfilledOrdersPlacedForTotalOfMostlyProduct( int $numberOfCustomers, int $numberOfOrders, - string $total, + int $total, ProductInterface $product, ): void { $this->createOrdersWithProduct($numberOfCustomers, $numberOfOrders, $total, $product, true); } /** - * @Given (then) :numberOfCustomers (more) customers have paid :numberOfOrders orders placed for total of :total + * @Given /^(\d+) (?:|more )new customers have paid (\d+) orders placed for total of ("[^"]+")$/ */ - public function thenMoreCustomersHavePaidOrdersPlacedForTotalOf( + public function moreCustomersHavePaidOrdersPlacedForTotalOf( int $numberOfCustomers, int $numberOfOrders, - string $total, + int $total, ): void { $this->createPaidOrders($numberOfCustomers, $numberOfOrders, $total); } @@ -647,15 +679,17 @@ final class OrderContext implements Context */ public function customerHasPlacedAnOrderBuyingASingleProductForOnTheChannel( CustomerInterface $customer, - $orderNumber, + string $orderNumber, ProductInterface $product, - $price, + int $price, ChannelInterface $channel, - ) { + ): void { $order = $this->createOrder($customer, $orderNumber, $channel); - $order->setState(OrderInterface::STATE_NEW); + $order->setState(BaseOrderInterface::STATE_NEW); - $this->addVariantWithPriceToOrder($order, $product->getVariants()->first(), $price); + $variant = $this->getProductVariant($product); + + $this->addVariantWithPriceToOrder($order, $variant, $price); $this->orderRepository->add($order); $this->sharedStorage->set('order', $order); @@ -665,7 +699,7 @@ final class OrderContext implements Context * @Given /^(this order) is already paid$/ * @Given the order :order is already paid */ - public function thisOrderIsAlreadyPaid(OrderInterface $order) + public function thisOrderIsAlreadyPaid(OrderInterface $order): void { $this->applyPaymentTransitionOnOrder($order, PaymentTransitions::TRANSITION_COMPLETE); @@ -676,7 +710,7 @@ final class OrderContext implements Context * @Given /^(this order) has been refunded$/ * @Given the customer has refunded the order with number :order */ - public function thisOrderHasBeenRefunded(OrderInterface $order) + public function thisOrderHasBeenRefunded(OrderInterface $order): void { $this->applyPaymentTransitionOnOrder($order, PaymentTransitions::TRANSITION_REFUND); @@ -689,9 +723,9 @@ final class OrderContext implements Context * @Given the order :order was cancelled * @Given /^I cancelled (this order)$/ */ - public function theCustomerCancelledThisOrder(OrderInterface $order) + public function theCustomerCancelledThisOrder(OrderInterface $order): void { - $this->stateMachineFactory->get($order, OrderTransitions::GRAPH)->apply(OrderTransitions::TRANSITION_CANCEL); + $this->stateMachine->apply($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CANCEL); $this->objectManager->flush(); } @@ -699,10 +733,10 @@ final class OrderContext implements Context /** * @Given /^I cancelled my last order$/ */ - public function theCustomerCancelledMyLastOrder() + public function theCustomerCancelledMyLastOrder(): void { $order = $this->sharedStorage->get('order'); - $this->stateMachineFactory->get($order, OrderTransitions::GRAPH)->apply(OrderTransitions::TRANSITION_CANCEL); + $this->stateMachine->apply($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CANCEL); $this->objectManager->flush(); } @@ -711,7 +745,7 @@ final class OrderContext implements Context * @Given /^(this order) has already been shipped$/ * @Given the order :order is already shipped */ - public function thisOrderHasAlreadyBeenShipped(OrderInterface $order) + public function thisOrderHasAlreadyBeenShipped(OrderInterface $order): void { $this->applyShipmentTransitionOnOrder($order, ShipmentTransitions::TRANSITION_SHIP); @@ -721,7 +755,7 @@ final class OrderContext implements Context /** * @When the customer used coupon :coupon */ - public function theCustomerUsedCoupon(PromotionCouponInterface $coupon) + public function theCustomerUsedCoupon(PromotionCouponInterface $coupon): void { /** @var OrderInterface $order */ $order = $this->sharedStorage->get('order'); @@ -765,56 +799,37 @@ final class OrderContext implements Context $this->objectManager->flush(); } - /** - * @param string $transition - */ - private function applyShipmentTransitionOnOrder(OrderInterface $order, $transition) + private function applyShipmentTransitionOnOrder(OrderInterface $order, string $transition): void { foreach ($order->getShipments() as $shipment) { - $this->stateMachineFactory->get($shipment, ShipmentTransitions::GRAPH)->apply($transition); + $this->stateMachine->apply($shipment, ShipmentTransitions::GRAPH, $transition); } } - /** - * @param string $transition - */ - private function applyPaymentTransitionOnOrder(OrderInterface $order, $transition) + private function applyPaymentTransitionOnOrder(OrderInterface $order, string $transition): void { foreach ($order->getPayments() as $payment) { - $this->stateMachineFactory->get($payment, PaymentTransitions::GRAPH)->apply($transition); + $this->stateMachine->apply($payment, PaymentTransitions::GRAPH, $transition); } } - /** - * @param string $transition - */ - private function applyTransitionOnOrderCheckout(OrderInterface $order, $transition) + private function applyTransitionOnOrderCheckout(OrderInterface $order, string $transition): void { - $this->stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH)->apply($transition); + $this->stateMachine->apply($order, OrderCheckoutTransitions::GRAPH, $transition); } - private function applyTransitionOnOrder(OrderInterface $order, string $transition): void - { - $this->stateMachineFactory->get($order, OrderTransitions::GRAPH)->apply($transition); - } - - /** - * @param int $quantity - * - * @return OrderInterface - */ private function addProductVariantToOrder( ProductVariantInterface $productVariant, - $quantity = 1, + int $quantity = 1, ?ChannelInterface $channel = null, - ) { + ): OrderInterface { $order = $this->sharedStorage->get('order'); $this->addProductVariantsToOrderWithChannelPrice( $order, $channel ?? $this->sharedStorage->get('channel'), $productVariant, - (int) $quantity, + $quantity, ); return $order; @@ -825,7 +840,7 @@ final class OrderContext implements Context ChannelInterface $channel, ProductVariantInterface $productVariant, int $quantity = 1, - ) { + ): void { /** @var OrderItemInterface $item */ $item = $this->orderItemFactory->createNew(); $item->setVariant($productVariant); @@ -839,19 +854,13 @@ final class OrderContext implements Context $order->addItem($item); } - /** - * @param string $number - * @param string|null $localeCode - * - * @return OrderInterface - */ private function createOrder( CustomerInterface $customer, - $number = null, - ChannelInterface $channel = null, - $localeCode = null, - ) { - $order = $this->createCart($customer, $channel, $localeCode); + ?string $number = null, + ?ChannelInterface $channel = null, + ): OrderInterface { + $order = $this->createCart($customer, $channel); + $order->setTokenValue($this->generateToken()); if (null !== $number) { $order->setNumber($number); @@ -862,22 +871,14 @@ final class OrderContext implements Context return $order; } - /** - * @param string|null $localeCode - * - * @return OrderInterface - */ - private function createCart( - CustomerInterface $customer, - ChannelInterface $channel = null, - $localeCode = null, - ) { + private function createCart(CustomerInterface $customer, ?ChannelInterface $channel = null): OrderInterface + { /** @var OrderInterface $order */ $order = $this->orderFactory->createNew(); $order->setCustomer($customer); $order->setChannel($channel ?? $this->sharedStorage->get('channel')); - $order->setLocaleCode($localeCode ?? $this->sharedStorage->get('locale')->getCode()); + $order->setLocaleCode($this->sharedStorage->get('locale')->getCode()); $order->setCurrencyCode($order->getChannel()->getBaseCurrency()->getCode()); return $order; @@ -903,11 +904,9 @@ final class OrderContext implements Context } /** - * @param int $count - * * @return CustomerInterface[] */ - private function generateCustomers($count) + private function generateCustomers(int $count): array { $customers = []; @@ -918,6 +917,8 @@ final class OrderContext implements Context $customer->setFirstname('John'); $customer->setLastname('Doe' . $i); + $customer->setCreatedAt($this->dateTimeProvider->now()); + $customers[] = $customer; $this->customerRepository->add($customer); @@ -926,17 +927,12 @@ final class OrderContext implements Context return $customers; } - private function getPriceFromString(string $price): int - { - return (int) round((float) str_replace(['€', '£', '$'], '', $price) * 100, 2); - } - private function checkoutUsing( OrderInterface $order, ShippingMethodInterface $shippingMethod, AddressInterface $address, PaymentMethodInterface $paymentMethod, - ) { + ): void { $order->setShippingAddress($address); $order->setBillingAddress(clone $address); @@ -946,7 +942,7 @@ final class OrderContext implements Context $this->completeCheckout($order); } - private function completeCheckout(OrderInterface $order) + private function completeCheckout(OrderInterface $order): void { $this->applyTransitionOnOrderCheckout($order, OrderCheckoutTransitions::TRANSITION_COMPLETE); } @@ -973,8 +969,11 @@ final class OrderContext implements Context $this->theCustomerChoseShippingWithPayment($shippingMethod, $paymentMethod); } - private function proceedSelectingShippingAndPaymentMethod(OrderInterface $order, ShippingMethodInterface $shippingMethod, PaymentMethodInterface $paymentMethod) - { + private function proceedSelectingShippingAndPaymentMethod( + OrderInterface $order, + ShippingMethodInterface $shippingMethod, + PaymentMethodInterface $paymentMethod, + ): void { foreach ($order->getShipments() as $shipment) { $shipment->setMethod($shippingMethod); } @@ -986,10 +985,7 @@ final class OrderContext implements Context $this->applyTransitionOnOrderCheckout($order, OrderCheckoutTransitions::TRANSITION_SELECT_PAYMENT); } - /** - * @param int $price - */ - private function addVariantWithPriceToOrder(OrderInterface $order, ProductVariantInterface $variant, $price) + private function addVariantWithPriceToOrder(OrderInterface $order, ProductVariantInterface $variant, int $price): void { /** @var OrderItemInterface $item */ $item = $this->orderItemFactory->createNew(); @@ -1004,16 +1000,15 @@ final class OrderContext implements Context private function createOrders( int $numberOfCustomers, int $numberOfOrders, - string $total, + int $total, bool $isFulfilled = false, ): void { $customers = $this->generateCustomers($numberOfCustomers); $sampleProductVariant = $this->sharedStorage->get('variant'); - $total = $this->getPriceFromString($total); for ($i = 0; $i < $numberOfOrders; ++$i) { $order = $this->createOrder($customers[random_int(0, $numberOfCustomers - 1)], '#' . uniqid()); - $this->stateMachineFactory->get($order, OrderTransitions::GRAPH)->apply(OrderTransitions::TRANSITION_CREATE); + $this->stateMachine->apply($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CREATE); $this->applyPaymentTransitionOnOrder($order, PaymentTransitions::TRANSITION_COMPLETE); $price = $i === ($numberOfOrders - 1) ? $total : random_int(1, $total); @@ -1026,6 +1021,8 @@ final class OrderContext implements Context $this->shipOrder($order); } + $order->setCheckoutCompletedAt($this->dateTimeProvider->now()); + $this->objectManager->persist($order); $this->sharedStorage->set('order', $order); } @@ -1033,15 +1030,14 @@ final class OrderContext implements Context $this->objectManager->flush(); } - private function createPaidOrders(int $numberOfCustomers, int $numberOfOrders, string $total): void + private function createPaidOrders(int $numberOfCustomers, int $numberOfOrders, int $total): void { $customers = $this->generateCustomers($numberOfCustomers); $sampleProductVariant = $this->sharedStorage->get('variant'); - $total = $this->getPriceFromString($total); for ($i = 0; $i < $numberOfOrders; ++$i) { $order = $this->createOrder($customers[random_int(0, $numberOfCustomers - 1)], '#' . uniqid()); - $this->stateMachineFactory->get($order, OrderTransitions::GRAPH)->apply(OrderTransitions::TRANSITION_CREATE); + $this->stateMachine->apply($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CREATE); $this->applyPaymentTransitionOnOrder($order, PaymentTransitions::TRANSITION_COMPLETE); $price = $i === ($numberOfOrders - 1) ? $total : random_int(1, $total); @@ -1061,17 +1057,18 @@ final class OrderContext implements Context private function createOrdersWithProduct( int $numberOfCustomers, int $numberOfOrders, - string $total, + int $total, ProductInterface $product, bool $isFulfilled = false, ): void { $customers = $this->generateCustomers($numberOfCustomers); + + /** @var ProductVariantInterface $sampleProductVariant */ $sampleProductVariant = $product->getVariants()->first(); - $total = $this->getPriceFromString($total); for ($i = 0; $i < $numberOfOrders; ++$i) { $order = $this->createOrder($customers[random_int(0, $numberOfCustomers - 1)], '#' . uniqid(), $product->getChannels()->first()); - $this->stateMachineFactory->get($order, OrderTransitions::GRAPH)->apply(OrderTransitions::TRANSITION_CREATE); + $this->stateMachine->apply($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CREATE); $this->applyPaymentTransitionOnOrder($order, PaymentTransitions::TRANSITION_COMPLETE); $price = $i === ($numberOfOrders - 1) ? $total : random_int(1, $total); @@ -1098,17 +1095,19 @@ final class OrderContext implements Context ProductInterface $product, bool $isFulfilled = false, ): void { + $variant = $this->getProductVariant($product); + for ($i = 0; $i < $orderCount; ++$i) { $order = $this->createOrder($customer, uniqid('#'), $channel); $this->addProductVariantsToOrderWithChannelPrice( $order, $channel, - $this->variantResolver->getVariant($product), - (int) $productCount, + $variant, + $productCount, ); - $order->setState($isFulfilled ? OrderInterface::STATE_FULFILLED : OrderInterface::STATE_NEW); + $order->setState($isFulfilled ? BaseOrderInterface::STATE_FULFILLED : BaseOrderInterface::STATE_NEW); $this->objectManager->persist($order); } @@ -1116,6 +1115,7 @@ final class OrderContext implements Context $this->objectManager->flush(); } + /** @return array */ private function getTargetPaymentTransitions(string $state): array { $state = strtolower($state); @@ -1140,10 +1140,8 @@ final class OrderContext implements Context CustomerInterface $customer, int $number, ): void { - /** @var ProductVariantInterface $variant */ - $variant = $this->variantResolver->getVariant($product); + $variant = $this->getProductVariant($product); - /** @var ChannelPricingInterface $channelPricing */ $channelPricing = $variant->getChannelPricingForChannel($this->sharedStorage->get('channel')); /** @var OrderItemInterface $item */ @@ -1163,13 +1161,34 @@ final class OrderContext implements Context $this->sharedStorage->set('order', $order); } + private function getProductVariant(ProductInterface $product): ProductVariantInterface + { + /** @var ProductVariantInterface|null $variant */ + $variant = $this->variantResolver->getVariant($product); + + if ($variant === null) { + throw new \RuntimeException(sprintf('Product "%s" has no variant', $product->getCode())); + } + + return $variant; + } + private function shipOrder(OrderInterface $order): void { - $this->stateMachineFactory->get($order, OrderShippingTransitions::GRAPH)->apply(OrderShippingTransitions::TRANSITION_SHIP); + $this->stateMachine->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_SHIP); } private function payOrder(OrderInterface $order): void { - $this->stateMachineFactory->get($order, OrderPaymentTransitions::GRAPH)->apply(OrderPaymentTransitions::TRANSITION_PAY); + $this->stateMachine->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_PAY); + } + + private function generateToken(): string + { + do { + $token = $this->randomnessGenerator->generateUriSafeString(10); + } while ($this->orderRepository->findOneBy(['tokenValue' => $token]) !== null); + + return $token; } } diff --git a/src/Sylius/Behat/Context/Setup/PriceHistoryContext.php b/src/Sylius/Behat/Context/Setup/PriceHistoryContext.php new file mode 100644 index 0000000000..6324041ae2 --- /dev/null +++ b/src/Sylius/Behat/Context/Setup/PriceHistoryContext.php @@ -0,0 +1,101 @@ +calendarContext->itIsNow($date); + + $channelPricing = $this->getChannelPricingFromProduct($product); + + $channelPricing->setPrice($price); + + $this->channelPricingManager->flush(); + } + + /** + * @Given /^on "([^"]+)" (its) original price changed to ("[^"]+")$/ + */ + public function onDayItsOriginalPriceChangedTo(string $date, ProductInterface $product, int $originalPrice): void + { + $this->calendarContext->itIsNow($date); + + $channelPricing = $this->getChannelPricingFromProduct($product); + + $channelPricing->setOriginalPrice($originalPrice); + + $this->channelPricingManager->flush(); + } + + /** + * @Given /^on "([^"]+)" (its) price changed to ("[^"]+") and original price to ("[^"]+")$/ + */ + public function onDayItsOriginalPriceChangedToAndOriginalPriceTo(string $date, ProductInterface $product, int $price, int $originalPrice): void + { + $this->calendarContext->itIsNow($date); + + $channelPricing = $this->getChannelPricingFromProduct($product); + + $channelPricing->setPrice($price); + $channelPricing->setOriginalPrice($originalPrice); + + $this->channelPricingManager->flush(); + } + + /** + * @Given /^on "([^"]+)" (its) original price has been removed$/ + */ + public function onDayItsOriginalPriceHasBeenRemoved(string $date, ProductInterface $product): void + { + $this->calendarContext->itIsNow($date); + + $channelPricing = $this->getChannelPricingFromProduct($product); + + $channelPricing->setOriginalPrice(null); + + $this->channelPricingManager->flush(); + } + + private function getChannelPricingFromProduct(ProductInterface $product): ChannelPricingInterface + { + $variant = $this->defaultVariantResolver->getVariant($product); + Assert::isInstanceOf($variant, ProductVariantInterface::class); + + $channelPricing = $variant->getChannelPricings()->first(); + Assert::isInstanceOf($channelPricing, ChannelPricingInterface::class); + + return $channelPricing; + } +} diff --git a/src/Sylius/Behat/Context/Setup/ProductAssociationContext.php b/src/Sylius/Behat/Context/Setup/ProductAssociationContext.php index cf4c6fc3d4..67e35ca6f7 100644 --- a/src/Sylius/Behat/Context/Setup/ProductAssociationContext.php +++ b/src/Sylius/Behat/Context/Setup/ProductAssociationContext.php @@ -140,6 +140,8 @@ final class ProductAssociationContext implements Context $product->addAssociation($productAssociation); $this->productAssociationRepository->add($productAssociation); + + $this->sharedStorage->set('product_association', $productAssociation); } private function addProductAssociationTypeTranslation( diff --git a/src/Sylius/Behat/Context/Setup/ProductAttributeContext.php b/src/Sylius/Behat/Context/Setup/ProductAttributeContext.php index b53ea28031..8210c99d3d 100644 --- a/src/Sylius/Behat/Context/Setup/ProductAttributeContext.php +++ b/src/Sylius/Behat/Context/Setup/ProductAttributeContext.php @@ -18,6 +18,8 @@ use Doctrine\Persistence\ObjectManager; use Faker\Factory; use Faker\Generator; use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Component\Attribute\AttributeType\DateAttributeType; +use Sylius\Component\Attribute\AttributeType\DatetimeAttributeType; use Sylius\Component\Attribute\AttributeType\SelectAttributeType; use Sylius\Component\Attribute\Factory\AttributeFactoryInterface; use Sylius\Component\Core\Formatter\StringInflector; @@ -63,7 +65,7 @@ final class ProductAttributeContext implements Context } /** - * @Given /^the store has(?:| also)(?:| a| an) (text|textarea|integer|percent) product attribute "([^"]+)"$/ + * @Given /^the store has(?:| also)(?:| a| an) (text|textarea|integer|percent|float) product attribute "([^"]+)"$/ */ public function theStoreHasAProductAttribute(string $type, string $name): void { @@ -73,7 +75,7 @@ final class ProductAttributeContext implements Context } /** - * @Given /^the store has(?:| also)(?:| a| an) non-translatable (text|textarea|integer|percent) product attribute "([^"]+)"$/ + * @Given /^the store has(?:| also)(?:| a| an) non-translatable (text|textarea|integer|percent|float) product attribute "([^"]+)"$/ */ public function theStoreHasANonTranslatableProductAttribute(string $type, string $name): void { @@ -157,6 +159,53 @@ final class ProductAttributeContext implements Context $this->saveProductAttribute($productAttribute); } + /** + * @Given the store has a non-translatable select product attribute :name with value :value + */ + public function theStoreHasANonTranslatableSelectProductAttributeWithValue(string $name, string $value): void + { + $choices[$this->faker->uuid] = ['en_US' => $value]; + + $productAttribute = $this->createProductAttribute(SelectAttributeType::TYPE, $name); + $productAttribute->setConfiguration([ + 'multiple' => true, + 'choices' => $choices, + 'min' => null, + 'max' => null, + ]); + $productAttribute->setTranslatable(false); + + $this->saveProductAttribute($productAttribute); + } + + /** + * @Given the store has a non-translatable date product attribute :name with format :format + */ + public function theStoreHasANonTranslatableDateProductAttributeWithFormat(string $name, string $format): void + { + $productAttribute = $this->createProductAttribute(DateAttributeType::TYPE, $name); + $productAttribute->setConfiguration([ + 'format' => $format, + ]); + $productAttribute->setTranslatable(false); + + $this->saveProductAttribute($productAttribute); + } + + /** + * @Given the store has a non-translatable datetime product attribute :name with format :format + */ + public function theStoreHasANonTranslatableDatetimeProductAttributeWithFormat(string $name, string $format): void + { + $productAttribute = $this->createProductAttribute(DatetimeAttributeType::TYPE, $name); + $productAttribute->setConfiguration([ + 'format' => $format, + ]); + $productAttribute->setTranslatable(false); + + $this->saveProductAttribute($productAttribute); + } + /** * @Given /^(this product attribute)'s "([^"]+)" value is labeled "([^"]+)" in the ("[^"]+" locale)$/ */ @@ -374,6 +423,59 @@ final class ProductAttributeContext implements Context $this->objectManager->flush(); } + /** + * @When /^(this product attribute)'s value changed from "([^"]+)" to "([^"]+)"$/ + */ + public function thisAttributeValueChangedFromTo( + ProductAttributeInterface $attribute, + string $from, + string $to, + ): void { + $configuration = $attribute->getConfiguration(); + $choices = $configuration['choices'] ?? []; + + foreach ($choices as $uuid => $choice) { + foreach ($choice as $localeCode => $item) { + if ($item === $from) { + $choices[$uuid][$localeCode] = $to; + + break 2; + } + } + } + + $configuration['choices'] = $choices; + $attribute->setConfiguration($configuration); + + $this->objectManager->flush(); + } + + /** + * @When /^(this product attribute)'s value "([^"]+)" has been removed$/ + */ + public function thisAttributeValueHasBeenRemoved( + ProductAttributeInterface $attribute, + string $value, + ): void { + $configuration = $attribute->getConfiguration(); + $choices = $configuration['choices'] ?? []; + + foreach ($choices as $uuid => $choice) { + foreach ($choice as $item) { + if ($value === $item) { + unset($choices[$uuid]); + + break 2; + } + } + } + + $configuration['choices'] = $choices; + $attribute->setConfiguration($configuration); + + $this->objectManager->flush(); + } + private function createProductAttribute( string $type, string $name, diff --git a/src/Sylius/Behat/Context/Setup/ProductContext.php b/src/Sylius/Behat/Context/Setup/ProductContext.php index 2bfce00fbe..3415878089 100644 --- a/src/Sylius/Behat/Context/Setup/ProductContext.php +++ b/src/Sylius/Behat/Context/Setup/ProductContext.php @@ -18,6 +18,7 @@ use Behat\Gherkin\Node\TableNode; use Behat\Mink\Element\NodeElement; use Doctrine\Persistence\ObjectManager; use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Component\Core\Event\ProductUpdated; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ChannelPricingInterface; @@ -28,6 +29,7 @@ use Sylius\Component\Core\Model\ProductTranslationInterface; use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Core\Repository\ProductRepositoryInterface; +use Sylius\Component\Core\Repository\ProductVariantRepositoryInterface; use Sylius\Component\Core\Uploader\ImageUploaderInterface; use Sylius\Component\Product\Factory\ProductFactoryInterface; use Sylius\Component\Product\Generator\ProductVariantGeneratorInterface; @@ -41,6 +43,7 @@ use Sylius\Component\Resource\Factory\FactoryInterface; use Sylius\Component\Shipping\Model\ShippingCategoryInterface; use Sylius\Component\Taxation\Model\TaxCategoryInterface; use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\Messenger\MessageBusInterface; use Webmozart\Assert\Assert; final class ProductContext implements Context @@ -59,10 +62,13 @@ final class ProductContext implements Context private FactoryInterface $productTaxonFactory, private ObjectManager $objectManager, private ProductVariantGeneratorInterface $productVariantGenerator, + private ProductVariantRepositoryInterface $productVariantRepository, private ProductVariantResolverInterface $defaultVariantResolver, private ImageUploaderInterface $imageUploader, private SlugGeneratorInterface $slugGenerator, private \ArrayAccess $minkParameters, + private MessageBusInterface $eventBus, + private ProductTaxonContext $productTaxonContext, ) { } @@ -73,13 +79,41 @@ final class ProductContext implements Context * @Given /^the store(?:| also) has a product "([^"]+)" priced at ("[^"]+")$/ * @Given /^the store(?:| also) has a product "([^"]+)" priced at ("[^"]+") in ("[^"]+" channel)$/ */ - public function storeHasAProductPricedAt($productName, int $price = 100, ChannelInterface $channel = null) + public function storeHasAProductPricedAt($productName, int $price = 100, ChannelInterface $channel = null): void { $product = $this->createProduct($productName, $price, $channel); $this->saveProduct($product); } + /** + * @Given /^the store(?:| also) has a product "([^"]+)" priced at ("[^"]+") belonging to the ("[^"]+" taxon)$/ + */ + public function storeHasAProductPricedAtBelongingToTheTaxon( + string $productName, + int $price, + TaxonInterface $taxon, + ): void { + $product = $this->createProduct($productName, $price); + $this->productTaxonContext->itBelongsTo($product, $taxon); + + $this->saveProduct($product); + } + + /** + * @Given /^the store(?:| also) has a product "([^"]+)" belonging to the ("[^"]+" taxon)$/ + * @Given /^the store(?:| also) has a product "([^"]+)" belonging to (this taxon)$/ + */ + public function storeHasAProductBelongingToTheTaxon( + string $productName, + TaxonInterface $taxon, + ): void { + $product = $this->createProduct($productName); + $this->productTaxonContext->itBelongsTo($product, $taxon); + + $this->saveProduct($product); + } + /** * @Given /^the store(?:| also) has a product "([^"]+)" in the ("[^"]+" taxon) at (\d+)(?:st|nd|rd|th) position$/ */ @@ -204,6 +238,16 @@ final class ProductContext implements Context $this->objectManager->flush(); } + /** + * @Given /^(this product) has no translation in the "([^"]+)" locale$/ + */ + public function thisProductHasNoTranslationIn(ProductInterface $product, $locale): void + { + $product->removeTranslation($product->getTranslation($locale)); + + $this->objectManager->flush(); + } + /** * @Given /^the store has a product named "([^"]+)" in ("[^"]+" locale) and "([^"]+)" in ("[^"]+" locale)$/ */ @@ -359,6 +403,7 @@ final class ProductContext implements Context /** * @Given /^the (product "[^"]+") has(?:| a| an) "([^"]+)" variant$/ * @Given /^(this product) has(?:| a| an) "([^"]+)" variant$/ + * @Given /^(this product) has "([^"]+)" and "([^"]+)" variants$/ * @Given /^(this product) has "([^"]+)", "([^"]+)" and "([^"]+)" variants$/ */ public function theProductHasVariants(ProductInterface $product, ...$variantNames) @@ -670,20 +715,17 @@ final class ProductContext implements Context /** * @Given /^(this product) is available in "([^"]+)" ([^"]+) priced at ("[^"]+")$/ */ - public function thisProductIsAvailableInSize(ProductInterface $product, $optionValueName, $optionName, int $price) + public function thisProductIsAvailableInSize(ProductInterface $product, string $optionValueName, string $optionName, int $price): void { - /** @var ProductVariantInterface $variant */ - $variant = $this->productVariantFactory->createNew(); + $this->createProductVariantWithOption($product, $optionName, $optionValueName, $price); + } - $optionValue = $this->sharedStorage->get(sprintf('%s_option_%s_value', $optionValueName, $optionName)); - - $variant->addOptionValue($optionValue); - $variant->addChannelPricing($this->createChannelPricingForChannel($price, $this->sharedStorage->get('channel'))); - $variant->setCode(sprintf('%s_%s', $product->getCode(), $optionValueName)); - $variant->setName($product->getName()); - - $product->addVariant($variant); - $this->objectManager->flush(); + /** + * @Given /^(this product) with "([^"]+)" option "([^"]+)" is priced at ("[^"]+")$/ + */ + public function thisProductWithOptionIsPricedAt(ProductInterface $product, string $optionName, string $optionValueName, int $price): void + { + $this->createProductVariantWithOption($product, $optionName, $optionValueName, $price); } /** @@ -771,10 +813,15 @@ final class ProductContext implements Context * @Given /^the (product "[^"]+") changed its price to ("[^"]+")$/ * @Given /^(this product) price has been changed to ("[^"]+")$/ */ - public function theProductChangedItsPriceTo(ProductInterface $product, int $price) + public function theProductChangedItsPriceTo(ProductInterface $product, int $price): void { - /** @var ProductVariantInterface $productVariant */ - $productVariant = $this->defaultVariantResolver->getVariant($product); + /** @var false|ProductInterface $productVariant */ + $productVariant = $product->getVariants()->first(); + Assert::isInstanceOf($productVariant, ProductVariantInterface::class); + + $productVariantId = $productVariant->getId(); + + $productVariant = $this->productVariantRepository->find($productVariantId); $channelPricing = $productVariant->getChannelPricingForChannel($this->sharedStorage->get('channel')); $channelPricing->setPrice($price); @@ -1095,6 +1142,8 @@ final class ProductContext implements Context */ public function theSizeColorVariantOfThisProductIsDisabled(ProductVariantInterface $productVariant): void { + /** @var ProductVariantInterface $productVariant */ + $productVariant = $this->productVariantRepository->find($productVariant->getId()); $productVariant->setEnabled(false); $this->objectManager->flush(); @@ -1134,6 +1183,162 @@ final class ProductContext implements Context $this->saveProduct($product); } + /** + * @Given /^(this product) has all possible variants priced at ("[^"]+") with indexed names$/ + */ + public function thisProductHasAllPossibleVariantsPricedAtWithIndexedNames( + ProductInterface $product, + int $price, + ): void { + try { + foreach ($product->getVariants() as $productVariant) { + $product->removeVariant($productVariant); + } + + $this->productVariantGenerator->generate($product); + } catch (\InvalidArgumentException) { + /** @var ProductVariantInterface $productVariant */ + $productVariant = $this->productVariantFactory->createNew(); + + $product->addVariant($productVariant); + } + + $i = 0; + /** @var ProductVariantInterface $productVariant */ + foreach ($product->getVariants() as $productVariant) { + $productVariant->setCode(sprintf('%s-variant-%d', $product->getCode(), $i)); + $productVariant->setName(sprintf('%s variant %d', $product->getName(), $i)); + + foreach ($product->getChannels() as $channel) { + $productVariant->addChannelPricing($this->createChannelPricingForChannel($price, $channel)); + } + + ++$i; + } + + $this->objectManager->flush(); + } + + /** + * @Given /^the ("[^"]+" product) is now priced at ("[^"]+") and originally priced at ("[^"]+")$/ + */ + public function theProductIsPricedAtAndOriginallyPricedAt( + ProductInterface $product, + int $price, + int $originalPrice, + ): void { + $channelPricing = $this->getChannelPricingFromProduct($product); + + $channelPricing->setPrice($price); + $channelPricing->setOriginalPrice($originalPrice); + + $this->saveProduct($product); + } + + /** + * @Given /^the (product "[^"]+") has a "([^"]+)" variant priced at ("[^"]+") and originally priced at ("[^"]+")$/ + */ + public function theProductHasVariantPricedAtAndOriginallyPricedAt( + ProductInterface $product, + string $productVariantName, + int $price, + int $originalPrice, + ): void { + /** @var ChannelPricingInterface $channelPricing */ + $channelPricing = $this->channelPricingFactory->createNew(); + $channelPricing->setPrice($price); + $channelPricing->setOriginalPrice($originalPrice); + $channelPricing->setChannelCode($this->sharedStorage->get('channel')->getCode()); + + /** @var ProductVariantInterface $variant */ + $variant = $this->productVariantFactory->createNew(); + $variant->setName($productVariantName); + $variant->setCode(StringInflector::nameToUppercaseCode($productVariantName)); + $variant->setProduct($product); + $variant->setOnHand(0); + $variant->addChannelPricing($channelPricing); + $variant->setShippingRequired(true); + + $product->setVariantSelectionMethod(ProductInterface::VARIANT_SELECTION_CHOICE); + $product->addVariant($variant); + + $this->saveProduct($product); + } + + /** + * @Given /^(this product)'s price changed to ("[^"]+")$/ + */ + public function thisProductsPriceChangedTo(ProductInterface $product, int $price): void + { + $channelPricing = $this->getChannelPricingFromProduct($product); + $channelPricing->setPrice($price); + + $this->saveProduct($product); + } + + /** + * @Given /^(this product)'s price changed to ("[^"]+") and original price changed to ("[^"]+")$/ + */ + public function thisProductsPriceChangedToAndOriginalPriceChangedTo( + ProductInterface $product, + int $price, + int $originalPrice, + ): void { + $channelPricing = $this->getChannelPricingFromProduct($product); + + $channelPricing->setPrice($price); + $channelPricing->setOriginalPrice($originalPrice); + + $this->saveProduct($product); + } + + /** + * @Given /^(this variant)'s price changed to ("[^"]+") and original price changed to ("[^"]+")$/ + */ + public function thisVariantsPriceChangedToAndOriginalPriceChangedTo( + ProductVariantInterface $productVariant, + int $price, + int $originalPrice, + ): void { + $channelPricing = $this->getChannelPricingFromVariant($productVariant); + + $channelPricing->setPrice($price); + $channelPricing->setOriginalPrice($originalPrice); + + /** @var ProductInterface $product */ + $product = $productVariant->getProduct(); + + $this->saveProduct($product); + } + + /** + * @Given /^(this product)'s price changed to ("[^"]+") and original price was removed$/ + */ + public function thisProductsPriceChangedToAndOriginalPriceWasRemoved(ProductInterface $product, $price): void + { + $channelPricing = $this->getChannelPricingFromProduct($product); + $channelPricing->setPrice($price); + $channelPricing->setOriginalPrice(null); + + $this->saveProduct($product); + } + + private function getChannelPricingFromProduct(ProductInterface $product): ChannelPricingInterface + { + $variant = $this->defaultVariantResolver->getVariant($product); + Assert::notNull($variant); + + return $this->getChannelPricingFromVariant($variant); + } + + private function getChannelPricingFromVariant(ProductVariantInterface $productVariant): ChannelPricingInterface + { + $channelPricing = $productVariant->getChannelPricings()->first(); + Assert::isInstanceOf($channelPricing, ChannelPricingInterface::class); + + return $channelPricing; + } + /** * @Given /^(this product) has no slug in the ("[^"]+" locale)$/ */ @@ -1163,12 +1368,7 @@ final class ProductContext implements Context return (int) round((float) str_replace(['€', '£', '$'], '', $price) * 100, 2); } - /** - * @param string $productName - * - * @return ProductInterface - */ - private function createProduct($productName, int $price = 100, ChannelInterface $channel = null) + private function createProduct(string $productName, int $price = 100, ChannelInterface $channel = null): ProductInterface { if (null === $channel && $this->sharedStorage->has('channel')) { $channel = $this->sharedStorage->get('channel'); @@ -1229,6 +1429,8 @@ final class ProductContext implements Context private function saveProduct(ProductInterface $product) { $this->productRepository->add($product); + $this->eventBus->dispatch(new ProductUpdated($product->getCode())); + $this->sharedStorage->set('variant', $product->getVariants()->first()); $this->sharedStorage->set('product', $product); } @@ -1286,6 +1488,25 @@ final class ProductContext implements Context return $variant; } + private function createProductVariantWithOption( + ProductInterface $product, + string $optionName, + string $optionValueName, + int $price, + ): void { + $variant = $this->productVariantFactory->createNew(); + + $optionValue = $this->sharedStorage->get(sprintf('%s_option_%s_value', $optionValueName, $optionName)); + + $variant->addOptionValue($optionValue); + $variant->addChannelPricing($this->createChannelPricingForChannel($price, $this->sharedStorage->get('channel'))); + $variant->setCode(sprintf('%s_%s', $product->getCode(), $optionValueName)); + $variant->setName($product->getName()); + + $product->addVariant($variant); + $this->objectManager->flush(); + } + /** * @param string $name * @param string $locale @@ -1306,6 +1527,14 @@ final class ProductContext implements Context $product->addTranslation($translation); } + private function removeProductTranslation(ProductInterface $product, $locale): void + { + /** @var ProductTranslationInterface $translation */ + $translation = $product->getTranslation($locale); + + $product->removeTranslation($translation); + } + /** * @param string $name * @param string $locale @@ -1320,10 +1549,7 @@ final class ProductContext implements Context $productVariant->addTranslation($translation); } - /** - * @return ChannelPricingInterface - */ - private function createChannelPricingForChannel(int $price, ChannelInterface $channel = null) + private function createChannelPricingForChannel(int $price, ChannelInterface $channel = null): ChannelPricingInterface { /** @var ChannelPricingInterface $channelPricing */ $channelPricing = $this->channelPricingFactory->createNew(); @@ -1341,10 +1567,10 @@ final class ProductContext implements Context $option->setName($optionName); $option->setCode(StringInflector::nameToUppercaseCode($optionName)); - $this->sharedStorage->set(sprintf('%s_option', $optionName), $option); + $this->sharedStorage->set(sprintf('%s_option', StringInflector::nameToLowercaseCode($optionName)), $option); foreach ($values as $value) { - $optionValue = $this->addProductOption($option, $value, StringInflector::nameToUppercaseCode($value)); + $optionValue = $this->addProductOption($option, $value, StringInflector::nameToCode($value)); $this->sharedStorage->set(sprintf('%s_option_%s_value', $value, strtolower($optionName)), $optionValue); } diff --git a/src/Sylius/Behat/Context/Setup/ProductReviewContext.php b/src/Sylius/Behat/Context/Setup/ProductReviewContext.php index 23e28c0986..15bd078576 100644 --- a/src/Sylius/Behat/Context/Setup/ProductReviewContext.php +++ b/src/Sylius/Behat/Context/Setup/ProductReviewContext.php @@ -14,7 +14,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Setup; use Behat\Behat\Context\Context; -use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ProductInterface; @@ -29,7 +29,7 @@ final class ProductReviewContext implements Context private SharedStorageInterface $sharedStorage, private FactoryInterface $productReviewFactory, private RepositoryInterface $productReviewRepository, - private StateMachineFactoryInterface $stateMachineFactory, + private StateMachineInterface $stateMachine, ) { } @@ -150,8 +150,7 @@ final class ProductReviewContext implements Context $product->addReview($review); if (null !== $transition) { - $stateMachine = $this->stateMachineFactory->get($review, ProductReviewTransitions::GRAPH); - $stateMachine->apply($transition); + $this->stateMachine->apply($review, ProductReviewTransitions::GRAPH, $transition); } $this->sharedStorage->set('product_review', $review); diff --git a/src/Sylius/Behat/Context/Setup/ProductTaxonContext.php b/src/Sylius/Behat/Context/Setup/ProductTaxonContext.php index 89b34aeead..2c8bea4ed7 100644 --- a/src/Sylius/Behat/Context/Setup/ProductTaxonContext.php +++ b/src/Sylius/Behat/Context/Setup/ProductTaxonContext.php @@ -43,6 +43,20 @@ final class ProductTaxonContext implements Context $this->objectManager->flush(); } + /** + * @Given /^(it|this product) (belongs to "[^"]+" and "[^"]+")$/ + */ + public function itBelongsToAnd(ProductInterface $product, iterable $taxons) + { + foreach ($taxons as $taxon) { + $productTaxon = $this->createProductTaxon($taxon, $product); + $product->addProductTaxon($productTaxon); + } + + $this->objectManager->persist($product); + $this->objectManager->flush(); + } + /** * @Given the product :product has a main taxon :taxon * @Given /^(this product) has a main (taxon "[^"]+")$/ @@ -53,12 +67,7 @@ final class ProductTaxonContext implements Context $this->objectManager->flush(); } - /** - * @param int|null $position - * - * @return ProductTaxonInterface - */ - private function createProductTaxon(TaxonInterface $taxon, ProductInterface $product, $position = null) + private function createProductTaxon(TaxonInterface $taxon, ProductInterface $product, ?int $position = null): ProductTaxonInterface { /** @var ProductTaxonInterface $productTaxon */ $productTaxon = $this->productTaxonFactory->createNew(); diff --git a/src/Sylius/Behat/Context/Setup/PromotionContext.php b/src/Sylius/Behat/Context/Setup/PromotionContext.php index b432d2cd68..ac28a9db6d 100644 --- a/src/Sylius/Behat/Context/Setup/PromotionContext.php +++ b/src/Sylius/Behat/Context/Setup/PromotionContext.php @@ -16,15 +16,19 @@ namespace Sylius\Behat\Context\Setup; use Behat\Behat\Context\Context; use Doctrine\Persistence\ObjectManager; use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Bundle\CoreBundle\Fixture\Factory\ExampleFactoryInterface; use Sylius\Component\Core\Factory\PromotionActionFactoryInterface; use Sylius\Component\Core\Factory\PromotionRuleFactoryInterface; +use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\PromotionCouponInterface; use Sylius\Component\Core\Model\PromotionInterface; use Sylius\Component\Core\Model\TaxonInterface; +use Sylius\Component\Core\Promotion\Checker\Rule\ContainsProductRuleChecker; use Sylius\Component\Core\Promotion\Checker\Rule\CustomerGroupRuleChecker; -use Sylius\Component\Core\Test\Factory\TestPromotionFactoryInterface; +use Sylius\Component\Core\Promotion\Checker\Rule\HasTaxonRuleChecker; +use Sylius\Component\Core\Promotion\Checker\Rule\TotalOfItemsFromTaxonRuleChecker; use Sylius\Component\Customer\Model\CustomerGroupInterface; use Sylius\Component\Promotion\Factory\PromotionCouponFactoryInterface; use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstruction; @@ -40,10 +44,10 @@ final class PromotionContext implements Context private PromotionActionFactoryInterface $actionFactory, private PromotionCouponFactoryInterface $couponFactory, private PromotionRuleFactoryInterface $ruleFactory, - private TestPromotionFactoryInterface $testPromotionFactory, private PromotionRepositoryInterface $promotionRepository, private PromotionCouponGeneratorInterface $couponGenerator, private ObjectManager $objectManager, + private ExampleFactoryInterface $promotionExampleFactory, ) { } @@ -54,19 +58,32 @@ final class PromotionContext implements Context */ public function thereIsPromotion(string $name, ?string $code = null): void { - $this->createPromotion($name, $code); + $this->createPromotion( + name: $name, + code: $code, + startsAt: (new \DateTime('-3 day'))->format('Y-m-d'), + endsAt: (new \DateTime('+3 day'))->format('Y-m-d'), + ); } /** * @Given /^there is a promotion "([^"]+)" with "Has at least one from taxons" rule (configured with "[^"]+" and "[^"]+")$/ */ - public function thereIsAPromotionWithHasAtLeastOneFromTaxonsRuleConfiguredWith(string $name, array $taxons): void + public function thereIsAPromotionWithHasAtLeastOneFromTaxonsRuleConfiguredWith(string $name, iterable $taxons): void { - $promotion = $this->createPromotion($name); - $rule = $this->ruleFactory->createHasTaxon([$taxons[0]->getCode(), $taxons[1]->getCode()]); - $promotion->addRule($rule); + $taxonCodes = array_map(fn (TaxonInterface $taxon) => $taxon->getCode(), iterator_to_array($taxons)); - $this->objectManager->flush(); + $this->createPromotion( + name: $name, + rules: [ + [ + 'type' => HasTaxonRuleChecker::TYPE, + 'configuration' => ['taxons' => $taxonCodes], + ], + ], + startsAt: (new \DateTime('-3 day'))->format('Y-m-d'), + endsAt: (new \DateTime('+3 day'))->format('Y-m-d'), + ); } /** @@ -78,11 +95,19 @@ final class PromotionContext implements Context int $amount, ChannelInterface $channel, ): void { - $promotion = $this->createPromotion($name); $rule = $this->ruleFactory->createItemsFromTaxonTotal($channel->getCode(), $taxon->getCode(), $amount); - $promotion->addRule($rule); - $this->objectManager->flush(); + $this->createPromotion( + name: $name, + rules: [ + [ + 'type' => TotalOfItemsFromTaxonRuleChecker::TYPE, + 'configuration' => [$channel->getCode() => ['taxon' => $taxon->getCode(), 'amount' => $amount]], + ], + ], + startsAt: (new \DateTime('-3 day'))->format('Y-m-d'), + endsAt: (new \DateTime('+3 day'))->format('Y-m-d'), + ); } /** @@ -90,14 +115,20 @@ final class PromotionContext implements Context */ public function thereIsAPromotionWithContainsProductRuleConfiguredWithProducts(string $name, array $products): void { - $promotion = $this->createPromotion($name); - + $rules = []; foreach ($products as $product) { - $rule = $this->ruleFactory->createContainsProduct($product->getCode()); - $promotion->addRule($rule); + $rules[] = [ + 'type' => ContainsProductRuleChecker::TYPE, + 'configuration' => ['product_code' => $product->getCode()], + ]; } - $this->objectManager->flush(); + $this->createPromotion( + name: $name, + rules: $rules, + startsAt: (new \DateTime('-3 day'))->format('Y-m-d'), + endsAt: (new \DateTime('+3 day'))->format('Y-m-d'), + ); } /** @@ -115,14 +146,12 @@ final class PromotionContext implements Context */ public function thereIsAPromotionWithPriority(string $promotionName, int $priority): void { - $promotion = $this->testPromotionFactory - ->createForChannel($promotionName, $this->sharedStorage->get('channel')) - ; - - $promotion->setPriority($priority); - - $this->promotionRepository->add($promotion); - $this->sharedStorage->set('promotion', $promotion); + $this->createPromotion( + name: $promotionName, + priority: $priority, + startsAt: (new \DateTime('-3 day'))->format('Y-m-d'), + endsAt: (new \DateTime('+3 day'))->format('Y-m-d'), + ); } /** @@ -130,15 +159,13 @@ final class PromotionContext implements Context */ public function thereIsAnExclusivePromotionWithPriority(string $promotionName, int $priority = 0): void { - $promotion = $this->testPromotionFactory - ->createForChannel($promotionName, $this->sharedStorage->get('channel')) - ; - - $promotion->setExclusive(true); - $promotion->setPriority($priority); - - $this->promotionRepository->add($promotion); - $this->sharedStorage->set('promotion', $promotion); + $this->createPromotion( + name: $promotionName, + priority: $priority, + exclusive: true, + startsAt: (new \DateTime('-3 day'))->format('Y-m-d'), + endsAt: (new \DateTime('+3 day'))->format('Y-m-d'), + ); } /** @@ -146,14 +173,12 @@ final class PromotionContext implements Context */ public function thereIsPromotionLimitedToUsages(string $promotionName, int $usageLimit): void { - $promotion = $this->testPromotionFactory - ->createForChannel($promotionName, $this->sharedStorage->get('channel')) - ; - - $promotion->setUsageLimit($usageLimit); - - $this->promotionRepository->add($promotion); - $this->sharedStorage->set('promotion', $promotion); + $this->createPromotion( + name: $promotionName, + usageLimit: $usageLimit, + startsAt: (new \DateTime('-3 day'))->format('Y-m-d'), + endsAt: (new \DateTime('+3 day'))->format('Y-m-d'), + ); } /** @@ -162,18 +187,20 @@ final class PromotionContext implements Context */ public function thereIsPromotionWithCoupon(string $promotionName, string $couponCode, ?int $usageLimit = null): void { - $coupon = $this->createCoupon($couponCode, $usageLimit); + $promotion = $this->createPromotion( + name: $promotionName, + coupons: [ + [ + 'code' => $couponCode, + 'usage_limit' => $usageLimit, + ], + ], + couponBased: true, + startsAt: (new \DateTime('-3 day'))->format('Y-m-d'), + endsAt: (new \DateTime('+3 day'))->format('Y-m-d'), + ); - $promotion = $this->testPromotionFactory - ->createForChannel($promotionName, $this->sharedStorage->get('channel')) - ; - $promotion->addCoupon($coupon); - $promotion->setCouponBased(true); - - $this->promotionRepository->add($promotion); - - $this->sharedStorage->set('promotion', $promotion); - $this->sharedStorage->set('coupon', $coupon); + $this->sharedStorage->set('coupon', $promotion->getCoupons()->first()); } /** @@ -181,12 +208,12 @@ final class PromotionContext implements Context */ public function thereIsAPromotionThatDoesNotApplyToDiscountedProducts(string $name): void { - $promotion = $this->createPromotion($name); - $promotion->setAppliesToDiscounted(false); - - $this->promotionRepository->add($promotion); - - $this->sharedStorage->set('promotion', $promotion); + $this->createPromotion( + name: $name, + appliesToDiscounted: false, + startsAt: (new \DateTime('-3 day'))->format('Y-m-d'), + endsAt: (new \DateTime('+3 day'))->format('Y-m-d'), + ); } /** @@ -254,6 +281,16 @@ final class PromotionContext implements Context $this->objectManager->flush(); } + /** + * @Given the promotion :promotion is archived + */ + public function thisPromotionIsArchived(PromotionInterface $promotion): void + { + $promotion->setArchivedAt(new \DateTime()); + + $this->objectManager->flush(); + } + /** * @Given /^(this coupon) has already expired$/ */ @@ -659,9 +696,11 @@ final class PromotionContext implements Context public function thePromotionGivesOffIfOrderContainsProductsClassifiedAsOr( PromotionInterface $promotion, int $discount, - array $taxons, + iterable $taxons, ): void { - $rule = $this->ruleFactory->createHasTaxon([$taxons[0]->getCode(), $taxons[1]->getCode()]); + $taxonCodes = array_map(fn (TaxonInterface $taxon) => $taxon->getCode(), iterator_to_array($taxons)); + + $rule = $this->ruleFactory->createHasTaxon($taxonCodes); $this->createFixedPromotion($promotion, $discount, [], $rule); } @@ -762,11 +801,11 @@ final class PromotionContext implements Context public function itGivesOffOnEveryProductClassifiedAsOrIfOrderContainsAnyProductClassifiedAsOr( PromotionInterface $promotion, float $discount, - array $discountTaxons, - array $targetTaxons, + iterable $discountTaxons, + iterable $targetTaxons, ): void { - $discountTaxonsCodes = [$discountTaxons[0]->getCode(), $discountTaxons[1]->getCode()]; - $targetTaxonsCodes = [$targetTaxons[0]->getCode(), $targetTaxons[1]->getCode()]; + $discountTaxonsCodes = array_map(fn (TaxonInterface $taxon) => $taxon->getCode(), iterator_to_array($discountTaxons)); + $targetTaxonsCodes = array_map(fn (TaxonInterface $taxon) => $taxon->getCode(), iterator_to_array($targetTaxons)); $rule = $this->ruleFactory->createHasTaxon($targetTaxonsCodes); @@ -992,14 +1031,46 @@ final class PromotionContext implements Context return $configuration; } - private function createPromotion(string $name, ?string $code = null): PromotionInterface - { - $promotion = $this->testPromotionFactory->createForChannel($name, $this->sharedStorage->get('channel')); - - if (null !== $code) { - $promotion->setCode($code); + private function createPromotion( + string $name, + ?string $description = null, + ?string $code = null, + array $channels = [], + ?array $rules = null, + ?array $actions = null, + array $coupons = [], + int $priority = null, + int $usageLimit = null, + bool $couponBased = false, + bool $exclusive = false, + bool $appliesToDiscounted = true, + ?string $startsAt = null, + ?string $endsAt = null, + ): PromotionInterface { + if (empty($channels) && $this->sharedStorage->has('channel')) { + $channels = [$this->sharedStorage->get('channel')]; } + $code ??= StringInflector::nameToCode($name); + + /** @var PromotionInterface $promotion */ + $promotion = $this->promotionExampleFactory->create([ + 'name' => $name, + 'description' => $description, + 'code' => $code, + 'channels' => $channels, + 'rules' => $rules, + 'actions' => $actions, + 'coupons' => $coupons, + 'priority' => $priority, + 'usage_limit' => $usageLimit, + 'coupon_based' => $couponBased, + 'exclusive' => $exclusive, + 'applies_to_discounted' => $appliesToDiscounted, + 'starts_at' => $startsAt, + 'ends_at' => $endsAt, + ]); + $this->promotionRepository->add($promotion); $this->sharedStorage->set('promotion', $promotion); diff --git a/src/Sylius/Behat/Context/Setup/ShippingContext.php b/src/Sylius/Behat/Context/Setup/ShippingContext.php index 84ce9bbc20..ac2efd4e4e 100644 --- a/src/Sylius/Behat/Context/Setup/ShippingContext.php +++ b/src/Sylius/Behat/Context/Setup/ShippingContext.php @@ -48,7 +48,7 @@ final class ShippingContext implements Context } /** - * @Given the store ships everything for free within the :zone zone + * @Given the store ships everything for Free within the :zone zone */ public function storeShipsEverythingForFree(ZoneInterface $zone): void { diff --git a/src/Sylius/Behat/Context/Setup/TaxonomyContext.php b/src/Sylius/Behat/Context/Setup/TaxonomyContext.php index 3948746b59..158033be75 100644 --- a/src/Sylius/Behat/Context/Setup/TaxonomyContext.php +++ b/src/Sylius/Behat/Context/Setup/TaxonomyContext.php @@ -147,12 +147,7 @@ final class TaxonomyContext implements Context $this->objectManager->flush(); } - /** - * @param string $name - * - * @return TaxonInterface - */ - private function createTaxon($name) + private function createTaxon(string $name): TaxonInterface { /** @var TaxonInterface $taxon */ $taxon = $this->taxonFactory->createNew(); diff --git a/src/Sylius/Behat/Context/Setup/UserContext.php b/src/Sylius/Behat/Context/Setup/UserContext.php index 7164eb1a2f..9bcfd7e285 100644 --- a/src/Sylius/Behat/Context/Setup/UserContext.php +++ b/src/Sylius/Behat/Context/Setup/UserContext.php @@ -31,6 +31,7 @@ final class UserContext implements Context private ExampleFactoryInterface $userFactory, private ObjectManager $userManager, private MessageBusInterface $messageBus, + private string $passwordResetTokenTtl, ) { } @@ -155,6 +156,26 @@ final class UserContext implements Context $this->userManager->flush(); } + /** + * @Given /^(I) waited too long, and the token expired$/ + */ + public function iWaitedTooLongAndTheTokenExpired(UserInterface $user): void + { + /** @var \DateTime $passwordRequestedAt */ + $passwordRequestedAt = $user->getPasswordRequestedAt(); + + // Subtracting the ttl twice because date operations tend to be wobbly + // and might result in random fails due to skip years, daylight saving + // time date changes, etc + $interval = new \DateInterval($this->passwordResetTokenTtl); + $passwordRequestedAt->sub($interval); + $passwordRequestedAt->sub($interval); + + $user->setPasswordRequestedAt($passwordRequestedAt); + + $this->userManager->flush(); + } + /** * @Given /^(I)'ve changed my password from "([^"]+)" to "([^"]+)"$/ */ diff --git a/src/Sylius/Behat/Context/Transform/AddressContext.php b/src/Sylius/Behat/Context/Transform/AddressContext.php index 0462e59043..32deb4956d 100644 --- a/src/Sylius/Behat/Context/Transform/AddressContext.php +++ b/src/Sylius/Behat/Context/Transform/AddressContext.php @@ -66,7 +66,7 @@ final class AddressContext implements Context } /** - * @Transform /^clear old (shipping|billing) address$/ + * @Transform /^clear the (shipping|billing) address$/ * @Transform /^do not specify any (shipping|billing) address$/ */ public function createEmptyAddress() @@ -104,6 +104,8 @@ final class AddressContext implements Context * @Transform /^of "([^"]+)" in the "([^"]+)", "([^"]+)" "([^"]+)", "([^"]+)"$/ * @Transform /^addressed it to "([^"]+)", "([^"]+)", "([^"]+)" "([^"]+)" in the "([^"]+)"$/ * @Transform /^address (?:|is |as )"([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)"$/ + * @Transform /^"([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)" as its(?:| new) billing address$/ + * @Transform /^be shipped to "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)"$/ */ public function createNewAddressWithName($name, $street, $postcode, $city, $countryName) { diff --git a/src/Sylius/Behat/Context/Transform/ChannelContext.php b/src/Sylius/Behat/Context/Transform/ChannelContext.php index 92a7cb38d5..cc1edad1b5 100644 --- a/src/Sylius/Behat/Context/Transform/ChannelContext.php +++ b/src/Sylius/Behat/Context/Transform/ChannelContext.php @@ -15,10 +15,14 @@ namespace Sylius\Behat\Context\Transform; use Behat\Behat\Context\Context; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; +use Sylius\Component\Core\Model\ChannelInterface; use Webmozart\Assert\Assert; final class ChannelContext implements Context { + /** + * @param ChannelRepositoryInterface $channelRepository + */ public function __construct(private ChannelRepositoryInterface $channelRepository) { } @@ -29,7 +33,7 @@ final class ChannelContext implements Context * @Transform /^channel to "([^"]+)"$/ * @Transform :channel */ - public function getChannelByName($channelName) + public function getChannelByName(string $channelName) { $channels = $this->channelRepository->findByName($channelName); @@ -44,8 +48,10 @@ final class ChannelContext implements Context /** * @Transform all channels + * + * @return array */ - public function getAllChannels() + public function getAllChannels(): array { return $this->channelRepository->findAll(); } diff --git a/src/Sylius/Behat/Context/Transform/LexicalContext.php b/src/Sylius/Behat/Context/Transform/LexicalContext.php index dbc9052290..2939d8376d 100644 --- a/src/Sylius/Behat/Context/Transform/LexicalContext.php +++ b/src/Sylius/Behat/Context/Transform/LexicalContext.php @@ -18,12 +18,13 @@ use Behat\Behat\Context\Context; final class LexicalContext implements Context { /** - * @Transform /^"(\-)?(?:€|£|¥|\$)((?:\d+\.)?\d+)"$/ + * @Transform /^"(\-)?(?:€|£|¥|\$)([0-9,]+(\.[0-9]*)?)"$/ */ public function getPriceFromString(string $sign, string $price): int { $this->validatePriceString($price); + $price = str_replace(',', '', $price); $price = (int) round((float) $price * 100, 2); if ('-' === $sign) { @@ -46,8 +47,12 @@ final class LexicalContext implements Context */ private function validatePriceString(string $price): void { - if (!(bool) preg_match('/^\d+(?:\.\d{1,2})?$/', $price)) { - throw new \InvalidArgumentException('Price string should not have more than 2 decimal digits.'); + if (!preg_match('/^.*\.\d{2}$/', $price)) { + throw new \InvalidArgumentException('The price string should have exactly 2 decimal digits.'); + } + + if (!preg_match('/^\d{1,3}(,\d{3})*\.\d{2}$/', $price)) { + throw new \InvalidArgumentException('Thousands and larger numbers should be separated by commas.'); } } } diff --git a/src/Sylius/Behat/Context/Transform/ProductAssociationTypeContext.php b/src/Sylius/Behat/Context/Transform/ProductAssociationTypeContext.php index b7f91d320e..188c8b9570 100644 --- a/src/Sylius/Behat/Context/Transform/ProductAssociationTypeContext.php +++ b/src/Sylius/Behat/Context/Transform/ProductAssociationTypeContext.php @@ -25,6 +25,7 @@ final class ProductAssociationTypeContext implements Context /** * @Transform /^association "([^"]+)"$/ + * @Transform /^associate as "([^"]+)"$/ * @Transform :productAssociationType */ public function getProductAssociationTypeByName($productAssociationTypeName) diff --git a/src/Sylius/Behat/Context/Transform/ProductAttributeContext.php b/src/Sylius/Behat/Context/Transform/ProductAttributeContext.php new file mode 100644 index 0000000000..53339f1c63 --- /dev/null +++ b/src/Sylius/Behat/Context/Transform/ProductAttributeContext.php @@ -0,0 +1,47 @@ +productAttributeTranslationRepository->findBy(['name' => $name]); + + Assert::notEmpty( + $productAttributeTranslations, + sprintf('Product attribute with with name "%s" does not exist', $name), + ); + + /** @var ProductAttributeInterface $productAttribute */ + $productAttribute = $productAttributeTranslations[0]->getTranslatable(); + + return $productAttribute; + } +} diff --git a/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php b/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php index f95a4268a0..493b43723a 100644 --- a/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php +++ b/src/Sylius/Behat/Context/Transform/ProductOptionValueContext.php @@ -26,6 +26,8 @@ final class ProductOptionValueContext implements Context /** * @Transform /^"([^"]+)" option value$/ + * @Transform :optionValue + * @Transform :productOptionValue */ public function getProductOptionValueByCode(string $code): ProductOptionValueInterface { diff --git a/src/Sylius/Behat/Context/Transform/ProductVariantContext.php b/src/Sylius/Behat/Context/Transform/ProductVariantContext.php index 1a762683e6..0d0c867cf1 100644 --- a/src/Sylius/Behat/Context/Transform/ProductVariantContext.php +++ b/src/Sylius/Behat/Context/Transform/ProductVariantContext.php @@ -32,6 +32,7 @@ final class ProductVariantContext implements Context /** * @Transform /^"([^"]+)" variant of product "([^"]+)"$/ + * @Transform /^"([^"]+)" variant of "([^"]+)" product$/ */ public function getProductVariantByNameAndProduct(string $variantName, string $productName): ProductVariantInterface { @@ -71,6 +72,8 @@ final class ProductVariantContext implements Context /** * @Transform /^"([^"]+)" product variant$/ * @Transform /^"([^"]+)" variant$/ + * @Transform /^variant "([^"]+)"$/ + * @Transform :productVariant * @Transform :variant */ public function getProductVariantByName($name) diff --git a/src/Sylius/Behat/Context/Transform/ShopUserContext.php b/src/Sylius/Behat/Context/Transform/ShopUserContext.php new file mode 100644 index 0000000000..57e8f98b1a --- /dev/null +++ b/src/Sylius/Behat/Context/Transform/ShopUserContext.php @@ -0,0 +1,38 @@ +shopUserRepository->findOneByEmail($email); + + Assert::notNull($shopUser, sprintf('Shop User with email "%s" does not exist', $email)); + + return $shopUser; + } +} diff --git a/src/Sylius/Behat/Context/Transform/TaxonContext.php b/src/Sylius/Behat/Context/Transform/TaxonContext.php index 66f32500cb..5bb7f995a5 100644 --- a/src/Sylius/Behat/Context/Transform/TaxonContext.php +++ b/src/Sylius/Behat/Context/Transform/TaxonContext.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Transform; use Behat\Behat\Context\Context; +use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Taxonomy\Repository\TaxonRepositoryInterface; use Webmozart\Assert\Assert; @@ -36,8 +37,9 @@ final class TaxonContext implements Context * @Transform /^taxon with "([^"]+)" name/ * @Transform /^taxon "([^"]+)"$/ * @Transform :taxon + * @Transform :parentTaxon */ - public function getTaxonByName($name) + public function getTaxonByName(string $name): TaxonInterface { $taxons = $this->taxonRepository->findByName($name, $this->locale); @@ -53,7 +55,7 @@ final class TaxonContext implements Context /** * @Transform /^taxon with "([^"]+)" code$/ */ - public function getTaxonByCode($code) + public function getTaxonByCode(string $code): TaxonInterface { $taxon = $this->taxonRepository->findOneBy(['code' => $code]); Assert::notNull($taxon, sprintf('Taxon with code "%s" does not exist.', $code)); @@ -65,12 +67,13 @@ final class TaxonContext implements Context * @Transform /^classified as "([^"]+)" or "([^"]+)"$/ * @Transform /^configured with "([^"]+)" and "([^"]+)"$/ * @Transform /^"([^"]+)" and "([^"]+)" taxons$/ + * @Transform /^belongs to "([^"]+)" and "([^"]+)"/ + * @Transform /^"([^"]+)" and "([^"]+)" in the vertical menu$/ */ - public function getTaxonsByNames(string $firstTaxonName, string $secondTaxonName): array + public function getTaxonsByNames(string ...$taxonNames): iterable { - return [ - $this->getTaxonByName($firstTaxonName), - $this->getTaxonByName($secondTaxonName), - ]; + foreach ($taxonNames as $taxonName) { + yield $this->getTaxonByName($taxonName); + } } } diff --git a/src/Sylius/Behat/Context/Ui/Admin/BrowsingProductVariantsContext.php b/src/Sylius/Behat/Context/Ui/Admin/BrowsingProductVariantsContext.php index d64bf1deb5..d1cf8ce07a 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/BrowsingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/BrowsingProductVariantsContext.php @@ -87,7 +87,7 @@ final class BrowsingProductVariantsContext implements Context /** * @When /^I browse variants of (this product)$/ * @When /^I (?:|want to )view all variants of (this product)$/ - * @When /^I view(?:| all) variants of the (product "[^"]+")$/ + * @When /^I view(?:| all) variants of the (product "[^"]+")(?:| again)$/ */ public function iWantToViewAllVariantsOfThisProduct(ProductInterface $product) { diff --git a/src/Sylius/Behat/Context/Ui/Admin/ChannelPricingLogEntryContext.php b/src/Sylius/Behat/Context/Ui/Admin/ChannelPricingLogEntryContext.php new file mode 100644 index 0000000000..711c4481a5 --- /dev/null +++ b/src/Sylius/Behat/Context/Ui/Admin/ChannelPricingLogEntryContext.php @@ -0,0 +1,73 @@ +getChannelPricings()->first(); + $product = $productVariant->getProduct(); + + $this->indexPage->open([ + 'productId' => $product->getId(), + 'variantId' => $productVariant->getId(), + 'channelPricingId' => $channelPricing->getId(), + ]); + } + + /** + * @Then I should see :count log entries in the catalog price history + * @Then I should see a single log entry in the catalog price history + */ + public function iShouldSeeLogEntriesInTheCatalogPriceHistoryForTheVariant(int $count = 1): void + { + Assert::same($this->indexPage->countItems(), $count); + } + + /** + * @Then /^there should be a log entry on the (\d+)(?:|st|nd|rd|th) position with the "([^"]+)" selling price, "([^"]+)" original price and datetime of the price change$/ + * @Then /^there should be a log entry on the (\d+)(?:|st|nd|rd|th) position with the "([^"]+)" selling price, no original price and datetime of the price change$/ + */ + public function thereShouldBeALogEntryOnThePositionWithTheSellingPriceOriginalPriceAndDatetimeOfThePriceChange( + int $position, + string $price, + string $originalPrice = '-', + ): void { + Assert::true($this->indexPage->isLogEntryWithPriceAndOriginalPriceOnPosition($price, $originalPrice, $position)); + } + + /** + * @Then /^there should be a log entry with the "([^"]+)" selling price, "([^"]+)" original price and datetime of the price change$/ + * @Then /^there should be a log entry with the "([^"]+)" selling price, no original price and datetime of the price change$/ + */ + public function thereShouldBeALogEntryWithTheSellingPriceOriginalPriceAndDatetimeOfThePriceChange( + string $price, + string $originalPrice = '-', + ): void { + Assert::true($this->indexPage->isLogEntryWithPriceAndOriginalPrice($price, $originalPrice)); + } +} diff --git a/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php b/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php index f99fd7f559..9fa375b2c7 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/DashboardContext.php @@ -16,7 +16,7 @@ namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; use FriendsOfBehat\PageObjectExtension\Page\UnexpectedPageException; use Sylius\Behat\Page\Admin\DashboardPageInterface; -use Sylius\Component\Core\Formatter\StringInflector; +use Sylius\Component\Core\Model\ChannelInterface; use Webmozart\Assert\Assert; final class DashboardContext implements Context @@ -28,8 +28,9 @@ final class DashboardContext implements Context /** * @Given I am on the administration dashboard * @When I (try to )open administration dashboard + * @When I (try to )view statistics */ - public function iOpenAdministrationDashboard(): void + public function iViewStatistics(): void { try { $this->dashboardPage->open(); @@ -38,17 +39,44 @@ final class DashboardContext implements Context } /** - * @When I open administration dashboard for :name channel + * @When I view statistics for :channel channel + * + * @throws UnexpectedPageException */ - public function iOpenAdministrationDashboardForChannel($name) + public function iViewStatisticsForChannel(ChannelInterface $channel): void { - $this->dashboardPage->open(['channel' => StringInflector::nameToLowercaseCode($name)]); + $this->dashboardPage->open(['channel' => $channel->getCode()]); + } + + /** + * @When /^I view statistics for ("[^"]+" channel) and (current|previous|next) year split by (month|day)$/ + * + * @throws UnexpectedPageException + */ + public function iViewStatisticsForChannelAndYear( + ChannelInterface $channel, + string $period, + string $interval, + ): void { + $this->dashboardPage->open(['channel' => $channel->getCode()]); + + match ($interval) { + 'month' => $this->dashboardPage->chooseYearSplitByMonthsInterval(), + 'day' => $this->dashboardPage->chooseMonthSplitByDaysInterval(), + default => throw new \InvalidArgumentException(sprintf('Interval "%s" is not supported.', $interval)), + }; + + match ($period) { + 'previous' => $this->dashboardPage->choosePreviousPeriod(), + 'next' => $this->dashboardPage->chooseNextPeriod(), + default => null, + }; } /** * @When I choose :channelName channel */ - public function iChooseChannel($channelName) + public function iChooseChannel(string $channelName): void { $this->dashboardPage->chooseChannel($channelName); } @@ -62,25 +90,25 @@ final class DashboardContext implements Context } /** - * @Then I should see :number new orders + * @Then I should see :number paid orders */ - public function iShouldSeeNewOrders($number) + public function iShouldSeeNewOrders(int $number): void { - Assert::same($this->dashboardPage->getNumberOfNewOrders(), (int) $number); + Assert::same($this->dashboardPage->getNumberOfNewOrders(), $number); } /** * @Then I should see :number new customers */ - public function iShouldSeeNewCustomers($number) + public function iShouldSeeNewCustomers(int $number): void { - Assert::same($this->dashboardPage->getNumberOfNewCustomers(), (int) $number); + Assert::same($this->dashboardPage->getNumberOfNewCustomers(), $number); } /** * @Then there should be total sales of :total */ - public function thereShouldBeTotalSalesOf($total) + public function thereShouldBeTotalSalesOf(string $total): void { Assert::same($this->dashboardPage->getTotalSales(), $total); } @@ -88,25 +116,29 @@ final class DashboardContext implements Context /** * @Then the average order value should be :value */ - public function myAverageOrderValueShouldBe($value) + public function myAverageOrderValueShouldBe(string $value): void { - Assert::same($this->dashboardPage->getAverageOrderValue(), $value); + Assert::same( + $this->dashboardPage->getAverageOrderValue(), + $value, + 'Expected average order value to be equal to %2$s, but it is %s.', + ); } /** * @Then I should see :number new customers in the list */ - public function iShouldSeeNewCustomersInTheList($number) + public function iShouldSeeNewCustomersInTheList(int $number): void { - Assert::same($this->dashboardPage->getNumberOfNewCustomersInTheList(), (int) $number); + Assert::same($this->dashboardPage->getNumberOfNewCustomersInTheList(), $number); } /** * @Then I should see :number new orders in the list */ - public function iShouldSeeNewOrdersInTheList($number) + public function iShouldSeeNewOrdersInTheList(int $number): void { - Assert::same($this->dashboardPage->getNumberOfNewOrdersInTheList(), (int) $number); + Assert::same($this->dashboardPage->getNumberOfNewOrdersInTheList(), $number); } /** diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingCatalogPromotionsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingCatalogPromotionsContext.php index 24b415a65e..d4bfa0e406 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingCatalogPromotionsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingCatalogPromotionsContext.php @@ -395,7 +395,6 @@ final class ManagingCatalogPromotionsContext implements Context public function iEditCatalogPromotionToHaveDiscount(CatalogPromotionInterface $catalogPromotion, string $amount): void { $this->updatePage->open(['id' => $catalogPromotion->getId()]); - $this->formElement->addAction(); $this->formElement->specifyLastActionDiscount($amount); $this->updatePage->saveChanges(); } @@ -494,7 +493,7 @@ final class ManagingCatalogPromotionsContext implements Context } /** - * @When I add fixed discount action without amount configured + * @When I add fixed discount action without amount configured for the :channel channel */ public function iAddFixedDiscountActionWithoutAmountConfigured(): void { @@ -701,25 +700,32 @@ final class ManagingCatalogPromotionsContext implements Context */ public function iShouldBeNotifiedThatADiscountAmountShouldBeBetween0And100Percent(): void { - Assert::same($this->formElement->getValidationMessage(), 'The percentage discount amount must be between 0% and 100%.'); + Assert::same( + $this->formElement->getValidationMessage(), + 'The percentage discount amount must be between 0% and 100%.', + ); } /** - * @Then I should be notified that a discount amount should be a number and cannot be empty + * @Then I should be notified that the percentage amount should be a number and cannot be empty */ - public function iShouldBeNotifiedThatADiscountAmountShouldBeANumber(): void + public function iShouldBeNotifiedThatThePercentageAmountShouldBeANumber(): void { - Assert::same($this->formElement->getValidationMessage(), 'The percentage discount amount must be a number and can not be empty.'); + Assert::same( + $this->formElement->getValidationMessage(), + 'The percentage discount amount must be a number and can not be empty.', + ); } /** - * @Then I should be notified that a discount amount should be configured for at least one channel + * @Then I should be notified that the fixed amount should be a number and cannot be empty */ - public function iShouldBeNotifiedThatADiscountAmountShouldBeConfiguredForAtLeasOneChannel(): void + public function iShouldBeNotifiedThatTheFixedAmountShouldBeANumber(): void { - Assert::true($this->formElement->hasOnlyOneValidationMessage( - 'Configuration for one of the required channels is not provided.', - )); + Assert::same( + $this->formElement->getValidationMessage(), + 'Provided configuration contains errors. Please add the fixed discount amount that is a number greater than 0.', + ); } /** @@ -1184,7 +1190,10 @@ final class ManagingCatalogPromotionsContext implements Context */ public function iShouldBeNotifiedThatNotAllChannelsAreFilled(): void { - Assert::same($this->formElement->getValidationMessage(), 'Configuration for one of the required channels is not provided.'); + Assert::contains( + $this->formElement->getValidationMessage(), + 'Provided configuration contains errors. Please add the fixed discount amount that is a number greater than 0.', + ); } /** @@ -1200,7 +1209,10 @@ final class ManagingCatalogPromotionsContext implements Context */ public function iShouldSeeTheCatalogPromotionScopeConfigurationForm(): void { - Assert::true($this->createPage->checkIfScopeConfigurationFormIsVisible(), 'Catalog promotion scope configuration form is not visible.'); + Assert::true( + $this->createPage->checkIfScopeConfigurationFormIsVisible(), + 'Catalog promotion scope configuration form is not visible.', + ); } /** @@ -1208,7 +1220,10 @@ final class ManagingCatalogPromotionsContext implements Context */ public function iShouldSeeTheCatalogPromotionActionConfigurationForm(): void { - Assert::true($this->createPage->checkIfActionConfigurationFormIsVisible(), 'Catalog promotion action configuration form is not visible.'); + Assert::true( + $this->createPage->checkIfActionConfigurationFormIsVisible(), + 'Catalog promotion action configuration form is not visible.', + ); } /** diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsBillingDataContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsBillingDataContext.php index 302623d43a..42eb04b045 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsBillingDataContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsBillingDataContext.php @@ -24,6 +24,22 @@ final class ManagingChannelsBillingDataContext implements Context { } + /** + * @When I specify shop billing data for this channel as :company, :street, :postcode, :city, :taxId tax ID and :country country + */ + public function iSpecifyNewShopBillingDataForChannelAs( + string $company, + string $street, + string $postcode, + string $city, + string $taxId, + CountryInterface $country, + ): void { + $this->shopBillingDataElement->specifyCompany($company); + $this->shopBillingDataElement->specifyTaxId($taxId); + $this->shopBillingDataElement->specifyBillingAddress($street, $postcode, $city, $country->getCode()); + } + /** * @When I specify company as :company */ @@ -69,6 +85,7 @@ final class ManagingChannelsBillingDataContext implements Context } /** + * @Then this channel shop billing address should be :street, :postcode :city and :country country * @Then this channel shop billing address should be :street, :postcode :city, :country */ public function thisChannelShopBillingAddressShouldBe( diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsContext.php index ce0937be41..d1d43bd43e 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingChannelsContext.php @@ -14,6 +14,9 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; +use Sylius\Behat\Element\Admin\Channel\DiscountedProductsCheckingPeriodInputElementInterface; +use Sylius\Behat\Element\Admin\Channel\ExcludeTaxonsFromShowingLowestPriceInputElementInterface; +use Sylius\Behat\Element\Admin\Channel\LowestPriceFlagElementInterface; use Sylius\Behat\Element\Admin\Channel\ShippingAddressInCheckoutRequiredElementInterface; use Sylius\Behat\NotificationType; use Sylius\Behat\Page\Admin\Channel\CreatePageInterface; @@ -23,6 +26,7 @@ use Sylius\Behat\Service\NotificationCheckerInterface; use Sylius\Behat\Service\Resolver\CurrentPageResolverInterface; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Currency\Model\CurrencyInterface; use Webmozart\Assert\Assert; @@ -35,6 +39,9 @@ final class ManagingChannelsContext implements Context private ShippingAddressInCheckoutRequiredElementInterface $shippingAddressInCheckoutRequiredElement, private CurrentPageResolverInterface $currentPageResolver, private NotificationCheckerInterface $notificationChecker, + private DiscountedProductsCheckingPeriodInputElementInterface $discountedProductsCheckingPeriodInputElement, + private LowestPriceFlagElementInterface $lowestPriceFlagElement, + private ExcludeTaxonsFromShowingLowestPriceInputElementInterface $excludeTaxonsFromShowingLowestPriceInputElement, ) { } @@ -220,6 +227,32 @@ final class ManagingChannelsContext implements Context $currentPage->disable(); } + /** + * @When I exclude the :taxon taxon from showing the lowest price of discounted products + */ + public function iExcludeTheTaxonFromShowingTheLowestPriceOfDiscountedProducts(TaxonInterface $taxon): void + { + $this->excludeTaxonsFromShowingLowestPriceInputElement->excludeTaxon($taxon); + } + + /** + * @When /^I exclude the ("([^"]+)" and "([^"]+)" taxons) from showing the lowest price of discounted products$/ + */ + public function iExcludeTheTaxonsFromShowingTheLowestPriceOfDiscountedProducts(iterable $taxons): void + { + foreach ($taxons as $taxon) { + $this->excludeTaxonsFromShowingLowestPriceInputElement->excludeTaxon($taxon); + } + } + + /** + * @When I remove the :taxon taxon from excluded taxons from showing the lowest price of discounted products + */ + public function iRemoveTheTaxonFromExcludedTaxonsFromShowingTheLowestPriceOfDiscountedProducts(TaxonInterface $taxon): void + { + $this->excludeTaxonsFromShowingLowestPriceInputElement->removeExcludedTaxon($taxon); + } + /** * @Then I should be notified that at least one channel has to be defined */ @@ -259,6 +292,7 @@ final class ManagingChannelsContext implements Context * @Given I am modifying a channel :channel * @When I want to modify a channel :channel * @When /^I want to modify (this channel)$/ + * @When I want to modify a billing data of channel :channel */ public function iWantToModifyChannel(ChannelInterface $channel): void { @@ -282,6 +316,7 @@ final class ManagingChannelsContext implements Context /** * @When I save my changes * @When I try to save my changes + * @When I save it */ public function iSaveMyChanges(): void { @@ -299,7 +334,7 @@ final class ManagingChannelsContext implements Context /** * @Then there should still be only one channel with :element :value */ - public function thereShouldStillBeOnlyOneChannelWithCode(string $element, string $value) + public function thereShouldStillBeOnlyOneChannelWithCode(string $element, string $value): void { $this->iWantToBrowseChannels(); @@ -342,6 +377,7 @@ final class ManagingChannelsContext implements Context /** * @Then the code field should be disabled + * @Then I should not be able to edit its code */ public function theCodeFieldShouldBeDisabled(): void { @@ -428,7 +464,7 @@ final class ManagingChannelsContext implements Context /** * @Then paying in :currencyCode should be possible for the :channel channel */ - public function payingInEuroShouldBePossibleForTheChannel(string $currencyCode, ChannelInterface $channel): void + public function payingInCurrencyShouldBePossibleForTheChannel(string $currencyCode, ChannelInterface $channel): void { $this->updatePage->open(['id' => $channel->getId()]); @@ -465,6 +501,81 @@ final class ManagingChannelsContext implements Context $currentPage->chooseTaxCalculationStrategy($taxCalculationStrategy); } + /** + * @When /^I specify (-?\d+) days as the lowest price for discounted products checking period$/ + */ + public function iSpecifyDaysAsTheLowestPriceForDiscountedProductsCheckingPeriod(int $days): void + { + $this->discountedProductsCheckingPeriodInputElement->specifyPeriod($days); + } + + /** + * @Then /^the "[^"]+" channel should have the lowest price for discounted products checking period set to (\d+) days$/ + * @Then its lowest price for discounted products checking period should be set to :days days + */ + public function theChannelShouldHaveTheLowestPriceForDiscountedProductsCheckingPeriodSetToDays(int $days): void + { + $lowestPriceForDiscountedProductsCheckingPeriod = $this->discountedProductsCheckingPeriodInputElement->getPeriod(); + + Assert::same($days, $lowestPriceForDiscountedProductsCheckingPeriod); + } + + /** + * @Then I should be notified that the lowest price for discounted products checking period must be lower + */ + public function iShouldBeNotifiedThatTheLowestPriceForDiscountedProductsCheckingPeriodMustBeLower(): void + { + Assert::same( + 'Value must be less than 2147483647', + $this->updatePage->getValidationMessage('discounted_products_checking_period'), + ); + } + + /** + * @When /^I (enable|disable) showing the lowest price of discounted products$/ + */ + public function iEnableShowingTheLowestPriceOfDiscountedProducts(string $visible): void + { + $this->lowestPriceFlagElement->$visible(); + } + + /** + * @Then I should be notified that the lowest price for discounted products checking period must be greater than 0 + */ + public function iShouldBeNotifiedThatTheLowestPriceForDiscountedProductsCheckingPeriodMustBeGreaterThanZero(): void + { + Assert::same( + 'Value must be greater than 0', + $this->updatePage->getValidationMessage('discounted_products_checking_period'), + ); + } + + /** + * @Then /^the ("[^"]+" channel) should have the lowest price of discounted products prior to the current discount (enabled|disabled)$/ + */ + public function theChannelShouldHaveTheLowestPriceOfDiscountedProductsPriorToTheCurrentDiscountEnabledOrDisabled( + ChannelInterface $channel, + string $visible, + ): void { + Assert::same( + 'enabled' === $visible, + $this->lowestPriceFlagElement->isEnabled(), + ); + } + + /** + * @Then /^this channel should have ("([^"]+)" and "([^"]+)" taxons) excluded from displaying the lowest price of discounted products$/ + */ + public function thisChannelShouldHaveTaxonsExcludedFromDisplayingTheLowestPriceOfDiscountedProducts(iterable $taxons): void + { + foreach ($taxons as $taxon) { + Assert::true( + $this->excludeTaxonsFromShowingLowestPriceInputElement->hasTaxonExcluded($taxon), + sprintf('The taxon with code %s should be excluded from displaying the lowest price of discounted products', $taxon->getCode()), + ); + } + } + /** * @Then the default tax zone for the :channel channel should be :taxZone */ @@ -499,6 +610,7 @@ final class ManagingChannelsContext implements Context /** * @Then the base currency field should be disabled + * @Then I should not be able to edit its base currency */ public function theBaseCurrencyFieldShouldBeDisabled(): void { @@ -529,6 +641,30 @@ final class ManagingChannelsContext implements Context Assert::same($this->updatePage->getMenuTaxon(), $menuTaxon); } + /** + * @Then this channel should have :taxon taxon excluded from displaying the lowest price of discounted products + */ + public function thisChannelShouldHaveTaxonExcludedFromDisplayingTheLowestPriceOfDiscountedProducts( + TaxonInterface $taxon, + ): void { + Assert::true( + $this->excludeTaxonsFromShowingLowestPriceInputElement->hasTaxonExcluded($taxon), + sprintf('The taxon with code %s should be excluded from displaying the lowest price of discounted products', $taxon->getCode()), + ); + } + + /** + * @Then this channel should not have :taxon taxon excluded from displaying the lowest price of discounted products + */ + public function thisChannelShouldNotHaveTaxonExcludedFromDisplayingTheLowestPriceOfDiscountedProducts( + TaxonInterface $taxon, + ): void { + Assert::false( + $this->excludeTaxonsFromShowingLowestPriceInputElement->hasTaxonExcluded($taxon), + sprintf('The taxon with code %s should be not be excluded from displaying the lowest price of discounted products', $taxon->getCode()), + ); + } + /** * @Then /^the required address in the checkout for this channel should be (billing|shipping)$/ */ diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingCountriesContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingCountriesContext.php index 391be67733..aaf583ae8a 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingCountriesContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingCountriesContext.php @@ -99,7 +99,7 @@ final class ManagingCountriesContext implements Context /** * @When I save my changes - * @When I try to save changes + * @When I try to save my changes */ public function iSaveMyChanges() { diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingCustomersContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingCustomersContext.php index 7234c922fc..709783c202 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingCustomersContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingCustomersContext.php @@ -90,6 +90,19 @@ final class ManagingCustomersContext implements Context $this->createPage->create(); } + /** + * @When I filter by group :groupName + * @When I filter by groups :firstGroup and :secondGroup + */ + public function iFilterByGroup(string ...$groupsNames): void + { + foreach ($groupsNames as $groupName) { + $this->indexPage->specifyFilterGroup($groupName); + } + + $this->indexPage->filter(); + } + /** * @Then the customer :customer should appear in the store * @Then the customer :customer should still have this email @@ -180,8 +193,9 @@ final class ManagingCustomersContext implements Context /** * @Then /^I should see (\d+) customers in the list$/ + * @Then /^I should see a single customer on the list$/ */ - public function iShouldSeeCustomersInTheList($amountOfCustomers) + public function iShouldSeeCustomersInTheList($amountOfCustomers = 1) { Assert::same($this->indexPage->countItems(), (int) $amountOfCustomers); } @@ -361,11 +375,11 @@ final class ManagingCustomersContext implements Context } /** - * @When I sort them by :sortBy + * @When I sort the orders :sortType by :field */ - public function iSortThemByChannel(string $sortBy): void + public function iSortTheOrderByField(string $field): void { - $this->ordersIndexPage->sort(ucfirst($sortBy)); + $this->ordersIndexPage->sort(ucfirst($field)); } /** @@ -390,9 +404,9 @@ final class ManagingCustomersContext implements Context } /** - * @Then his name should be :name + * @Then /^(?:their|his) name should be "([^"]+)"$/ */ - public function hisNameShouldBe($name) + public function hisNameShouldBe(string $name): void { Assert::same($this->showPage->getCustomerName(), $name); } @@ -406,29 +420,46 @@ final class ManagingCustomersContext implements Context } /** - * @Then his email should be :email + * @Then /^(?:their|his) email should be "([^"]+)"$/ */ - public function hisEmailShouldBe($email) + public function hisEmailShouldBe(string $email): void { Assert::same($this->showPage->getCustomerEmail(), $email); } /** - * @Then his phone number should be :phoneNumber + * @Then /^(?:their|his) phone number should be "([^"]+)"$/ */ - public function hisPhoneNumberShouldBe($phoneNumber) + public function hisPhoneNumberShouldBe(string $phoneNumber): void { Assert::same($this->showPage->getCustomerPhoneNumber(), $phoneNumber); } /** - * @Then his default address should be :defaultAddress + * @Then /^(?:their|his) default address should be "([^"]+)"$/ */ - public function hisShippingAddressShouldBe($defaultAddress) + public function hisShippingAddressShouldBe(string $defaultAddress): void { Assert::same($this->showPage->getDefaultAddress(), str_replace(',', '', $defaultAddress)); } + /** + * @Then their default address should be :firstName :lastName, :street, :postcode :city, :country + */ + public function theirSDefaultAddressShouldBe( + string $firstName, + string $lastName, + string $street, + string $postcode, + string $city, + string $country, + ): void { + Assert::same( + $this->showPage->getDefaultAddress(), + sprintf('%s %s %s %s %s %s', $firstName, $lastName, $street, $city, strtoupper($country), $postcode), + ); + } + /** * @Then I should see information about no existing account for this customer */ diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingLocalesContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingLocalesContext.php index fadc86225f..800a9ee478 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingLocalesContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingLocalesContext.php @@ -14,8 +14,11 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; +use Sylius\Behat\NotificationType; use Sylius\Behat\Page\Admin\Crud\IndexPageInterface; use Sylius\Behat\Page\Admin\Locale\CreatePageInterface; +use Sylius\Behat\Service\NotificationCheckerInterface; +use Sylius\Component\Locale\Converter\LocaleConverterInterface; use Webmozart\Assert\Assert; final class ManagingLocalesContext implements Context @@ -23,6 +26,8 @@ final class ManagingLocalesContext implements Context public function __construct( private CreatePageInterface $createPage, private IndexPageInterface $indexPage, + private NotificationCheckerInterface $notificationChecker, + private LocaleConverterInterface $localeConverter, ) { } @@ -51,6 +56,15 @@ final class ManagingLocalesContext implements Context $this->createPage->create(); } + /** + * @When I remove :localeCode locale + */ + public function iRemoveLocale(string $localeCode): void + { + $this->indexPage->open(); + $this->indexPage->deleteResourceOnPage(['code' => $localeCode]); + } + /** * @Then the store should be available in the :name language */ @@ -68,4 +82,43 @@ final class ManagingLocalesContext implements Context { Assert::false($this->createPage->isOptionAvailable($name)); } + + /** + * @Then I should be informed that locale :localeCode has been deleted + */ + public function iShouldBeInformedThatLocaleHasBeenDeleted(string $localeCode): void + { + $this->notificationChecker->checkNotification( + 'Locale has been successfully deleted.', + NotificationType::success(), + ); + } + + /** + * @Then only the :localeCode locale should be present in the system + */ + public function onlyTheLocaleShouldBePresentInTheSystem(string $localeCode): void + { + Assert::true($this->indexPage->isSingleResourceOnPage(['code' => $localeCode])); + Assert::true($this->indexPage->countItems() === 1); + } + + /** + * @Then I should be informed that locale :localeCode is in use and cannot be deleted + */ + public function iShouldBeInformedThatLocaleIsInUseAndCannotBeDeleted(string $localeCode): void + { + $this->notificationChecker->checkNotification( + 'Cannot delete the locale, as it is used by at least one translation.', + NotificationType::failure(), + ); + } + + /** + * @Then the :localeCode locale should be still present in the system + */ + public function theLocaleShouldBeStillPresentInTheSystem(string $localeCode): void + { + Assert::true($this->indexPage->isSingleResourceOnPage(['code' => $localeCode])); + } } diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingOrdersContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingOrdersContext.php index 0d78416ca3..a13e575d01 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingOrdersContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingOrdersContext.php @@ -232,6 +232,17 @@ final class ManagingOrdersContext implements Context Assert::true($this->indexPage->isSingleResourceOnPage(['customer' => $customer->getEmail()])); } + /** + * @Then I should not be able to resend the shipment confirmation email + */ + public function iShouldNotBeAbleToResendTheShipmentConfirmationEmail(): void + { + Assert::false( + $this->showPage->isResendShipmentConfirmationEmailButtonVisible(), + 'Resend shipment confirmation email button should not be visible.', + ); + } + /** * @Then I should see a single order in the list */ @@ -290,23 +301,20 @@ final class ManagingOrdersContext implements Context string $city, string $countryName, ) { - $this->itShouldBeBilledTo(null, $customerName, $street, $postcode, $city, $countryName); + Assert::true($this->showPage->hasBillingAddress($customerName, $street, $postcode, $city, $countryName)); } /** - * @Then /^(this order) bill should (?:|still )be shipped to "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)"$/ + * @Then /^(?:it|this order) should(?:| still) have "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)", "([^"]+)" as its(?:| new) billing address$/ */ - public function itShouldBeBilledTo( - ?OrderInterface $order, + public function itShouldHaveAsItsBillingAddress( string $customerName, string $street, string $postcode, string $city, string $countryName, - ) { - if (null !== $order) { - $this->iSeeTheOrder($order); - } + ): void { + $this->iSeeTheOrder($this->sharedStorage->get('order')); Assert::true($this->showPage->hasBillingAddress($customerName, $street, $postcode, $city, $countryName)); } @@ -791,14 +799,22 @@ final class ManagingOrdersContext implements Context } /** - * @Then /^I should be notified that the "([^"]+)", the "([^"]+)", the "([^"]+)" and the "([^"]+)" in (shipping|billing) details are required$/ + * @Then /^I should be notified that all mandatory (shipping|billing) address details are incomplete$/ */ - public function iShouldBeNotifiedThatTheAndTheInShippingDetailsAreRequired($firstElement, $secondElement, $thirdElement, $fourthElement, $type) + public function iShouldBeNotifiedThatAllMandatoryAddressDetailsAreIncomplete(string $type): void { - $this->assertElementValidationMessage($type, $firstElement, sprintf('Please enter %s.', $firstElement)); - $this->assertElementValidationMessage($type, $secondElement, sprintf('Please enter %s.', $secondElement)); - $this->assertElementValidationMessage($type, $thirdElement, sprintf('Please enter %s.', $thirdElement)); - $this->assertElementValidationMessage($type, $fourthElement, sprintf('Please enter %s.', $fourthElement)); + /** @var array $mandatoryAddressFields */ + $mandatoryAddressFields = ['first name', 'last name', 'street', 'city', 'postcode']; + + foreach ($mandatoryAddressFields as $mandatoryAddressField) { + $this->assertElementValidationMessage( + $type, + $mandatoryAddressField, + sprintf('Please enter %s.', $mandatoryAddressField), + ); + } + + $this->assertElementValidationMessage($type, 'country', 'Please select country.'); } /** @@ -810,7 +826,7 @@ final class ManagingOrdersContext implements Context } /** - * @Then I should see :provinceName ad province in the billing address + * @Then I should see :provinceName as province in the billing address */ public function iShouldSeeAdProvinceInTheBillingAddress($provinceName) { @@ -835,17 +851,17 @@ final class ManagingOrdersContext implements Context } /** - * @When /^I (clear old billing address) information$/ + * @When /^I (clear the billing address) information$/ */ - public function iSpecifyTheBillingAddressAs(AddressInterface $address) + public function iClearTheBillingAddressInformation(AddressInterface $address) { $this->updatePage->specifyBillingAddress($address); } /** - * @When /^I (clear old shipping address) information$/ + * @When /^I (clear the shipping address) information$/ */ - public function iSpecifyTheShippingAddressAs(AddressInterface $address) + public function iClearTheShippingAddressInformation(AddressInterface $address) { $this->updatePage->specifyShippingAddress($address); } @@ -853,7 +869,7 @@ final class ManagingOrdersContext implements Context /** * @When /^I do not specify new information$/ */ - public function iDoNotSpecifyNewInformation() + public function iDoNotSpecifyNewInformation(): void { // Intentionally left blank to fulfill context expectation } @@ -883,11 +899,19 @@ final class ManagingOrdersContext implements Context } /** - * @Then there should be :count changes in the registry + * @Then there should be :count shipping address changes in the registry */ - public function thereShouldBeCountChangesInTheRegistry($count) + public function thereShouldBeCountShippingAddressChangesInTheRegistry(int $count): void { - Assert::same($this->historyPage->countShippingAddressChanges(), (int) $count); + Assert::same($this->historyPage->countShippingAddressChanges(), $count); + } + + /** + * @Then there should be :count billing address changes in the registry + */ + public function thereShouldBeCountBillingAddressChangesInTheRegistry(int $count): void + { + Assert::same($this->historyPage->countBillingAddressChanges(), $count); } /** @@ -974,6 +998,17 @@ final class ManagingOrdersContext implements Context ); } + /** + * @Then I should not be able to resend the order confirmation email + */ + public function iShouldNotBeAbleToResendTheOrderConfirmationEmail(): void + { + Assert::false( + $this->showPage->isResendOrderConfirmationEmailButtonVisible(), + 'Resend order confirmation email button should not be visible.', + ); + } + /** * @Then I should see the shipping date as :dateTime */ diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentMethodsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentMethodsContext.php index 307eed3e06..f2efc46604 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentMethodsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentMethodsContext.php @@ -14,7 +14,6 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; -use Sylius\Behat\NotificationType; use Sylius\Behat\Page\Admin\Crud\IndexPageInterface; use Sylius\Behat\Page\Admin\PaymentMethod\CreatePageInterface; use Sylius\Behat\Page\Admin\PaymentMethod\UpdatePageInterface; @@ -38,7 +37,7 @@ final class ManagingPaymentMethodsContext implements Context /** * @When I want to modify the :paymentMethod payment method */ - public function iWantToModifyAPaymentMethod(PaymentMethodInterface $paymentMethod) + public function iWantToModifyAPaymentMethod(PaymentMethodInterface $paymentMethod): void { $this->updatePage->open(['id' => $paymentMethod->getId()]); } @@ -99,14 +98,6 @@ final class ManagingPaymentMethodsContext implements Context $this->indexPage->deleteResourceOnPage(['code' => $paymentMethod->getCode(), 'name' => $paymentMethod->getName()]); } - /** - * @Then I should be notified that it is in use - */ - public function iShouldBeNotifiedThatItIsInUse() - { - $this->notificationChecker->checkNotification('Cannot delete, the Payment method is in use.', NotificationType::failure()); - } - /** * @Then this payment method :element should be :value */ @@ -256,6 +247,7 @@ final class ManagingPaymentMethodsContext implements Context /** * @When I switch the way payment methods are sorted by :field * @When I start sorting payment methods by :field + * @When I switch the way payment methods are sorted to descending by :field * @Given the payment methods are already sorted by :field */ public function iSortPaymentMethodsBy($field) @@ -274,6 +266,7 @@ final class ManagingPaymentMethodsContext implements Context /** * @Then I should be notified that :element is required + * @Then I should be notified that I have to specify payment method :element */ public function iShouldBeNotifiedThatIsRequired($element) { @@ -291,6 +284,17 @@ final class ManagingPaymentMethodsContext implements Context ); } + /** + * @Then I should be notified that I have to specify stripe :element + */ + public function iShouldBeNotifiedThatIHaveToSpecifyStripe(string $element): void + { + Assert::same( + $this->createPage->getValidationMessage('stripe_' . str_replace(' ', '_', $element)), + sprintf('Please enter stripe %s.', $element), + ); + } + /** * @Then I should be notified that gateway name should contain only letters and underscores */ @@ -339,6 +343,7 @@ final class ManagingPaymentMethodsContext implements Context /** * @Then the code field should be disabled + * @Then I should not be able to edit its code */ public function theCodeFieldShouldBeDisabled() { @@ -465,6 +470,17 @@ final class ManagingPaymentMethodsContext implements Context $this->createPage->setStripePublishableKey('TEST'); } + /** + * @When I configure it with only :element + */ + public function iConfigureItWithOnly(string $element): void + { + match ($element) { + 'publishable key' => $this->createPage->setStripePublishableKey('TEST'), + 'secret key' => $this->createPage->setStripeSecretKey('TEST'), + }; + } + /** * @Then I should be redirected to the previous page of only enabled payment methods */ diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentsContext.php index 939991e430..4266942e52 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingPaymentsContext.php @@ -108,6 +108,7 @@ final class ManagingPaymentsContext implements Context /** * @Then I should see order page with details of order :order + * @Then I should see the details of order :order */ public function iShouldSeeOrderPageWithDetailsOfOrder(OrderInterface $order): void { diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductAttributesContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductAttributesContext.php index e7bf2a9a20..cbc2367993 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductAttributesContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductAttributesContext.php @@ -18,8 +18,6 @@ use Sylius\Behat\Page\Admin\Crud\IndexPageInterface; use Sylius\Behat\Page\Admin\ProductAttribute\CreatePageInterface; use Sylius\Behat\Page\Admin\ProductAttribute\UpdatePageInterface; use Sylius\Behat\Service\Resolver\CurrentPageResolverInterface; -use Sylius\Behat\Service\SharedSecurityServiceInterface; -use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Product\Model\ProductAttributeInterface; use Webmozart\Assert\Assert; @@ -30,7 +28,6 @@ final class ManagingProductAttributesContext implements Context private IndexPageInterface $indexPage, private UpdatePageInterface $updatePage, private CurrentPageResolverInterface $currentPageResolver, - private SharedSecurityServiceInterface $sharedSecurityService, ) { } @@ -115,6 +112,7 @@ final class ManagingProductAttributesContext implements Context /** * @Then the :type attribute :name should appear in the store + * @Then the :type attribute :name should still be in the store */ public function theAttributeShouldAppearInTheStore($type, $name) { @@ -152,17 +150,18 @@ final class ManagingProductAttributesContext implements Context } /** - * @Then the code field should be disabled + * @Then I should not be able to edit its code */ - public function theCodeFieldShouldBeDisabled() + public function iShouldNotBeAbleToEditItsCode(): void { Assert::true($this->updatePage->isCodeDisabled()); } /** * @Then the type field should be disabled + * @Then I should not be able to edit its type */ - public function theTypeFieldShouldBeDisabled() + public function theTypeFieldShouldBeDisabled(): void { /** @var CreatePageInterface|UpdatePageInterface $currentPage */ $currentPage = $this->currentPageResolver->getCurrentPageWithForm([$this->createPage, $this->updatePage]); @@ -231,25 +230,6 @@ final class ManagingProductAttributesContext implements Context $this->indexPage->open(); } - /** - * @When /^(the administrator) changes (this product attribute)'s value "([^"]*)" to "([^"]*)"$/ - */ - public function theAdministratorChangesThisProductAttributesValueTo( - AdminUserInterface $user, - ProductAttributeInterface $productAttribute, - string $oldValue, - string $newValue, - ): void { - $this->sharedSecurityService->performActionAsAdminUser( - $user, - function () use ($productAttribute, $oldValue, $newValue) { - $this->iWantToEditThisAttribute($productAttribute); - $this->iChangeItsValueTo($oldValue, $newValue); - $this->iSaveMyChanges(); - }, - ); - } - /** * @When I specify its min length as :min * @When I specify its min entries value as :min @@ -284,24 +264,6 @@ final class ManagingProductAttributesContext implements Context // Intentionally left blank to fulfill context expectation } - /** - * @When /^(the administrator) deletes the value "([^"]+)" from (this product attribute)$/ - */ - public function theAdministratorDeletesTheValueFromThisProductAttribute( - AdminUserInterface $user, - string $value, - ProductAttributeInterface $productAttribute, - ): void { - $this->sharedSecurityService->performActionAsAdminUser( - $user, - function () use ($productAttribute, $value) { - $this->iWantToEditThisAttribute($productAttribute); - $this->iDeleteValue($value); - $this->iSaveMyChanges(); - }, - ); - } - /** * @When I check (also) the :productAttributeName product attribute */ @@ -328,7 +290,7 @@ final class ManagingProductAttributesContext implements Context } /** - * @When /^I delete (this product attribute)$/ + * @When /^I(?:| try to) delete (this product attribute)$/ */ public function iDeleteThisProductAttribute(ProductAttributeInterface $productAttribute) { diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductReviewsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductReviewsContext.php index bb944f89b7..b0f166ca4a 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductReviewsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductReviewsContext.php @@ -47,6 +47,22 @@ final class ManagingProductReviewsContext implements Context $this->indexPage->checkResourceOnPage(['title' => $productReviewTitle]); } + /** + * @When I choose :state as a status filter + */ + public function iChooseStateAsStatusFilter(string $state): void + { + $this->indexPage->chooseState($state); + } + + /** + * @When I filter + */ + public function iFilter(): void + { + $this->indexPage->filter(); + } + /** * @When I delete them */ diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductTaxonsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductTaxonsContext.php index 6ae2862133..2628a21596 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductTaxonsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductTaxonsContext.php @@ -25,9 +25,10 @@ final class ManagingProductTaxonsContext implements Context } /** - * @When I change that the :product product belongs to the :taxon taxon + * @When I add :taxon taxon to the :product product + * @When I assign the :taxon taxon to the :product product */ - public function iChangeThatTheProductBelongsToTheTaxon(ProductInterface $product, TaxonInterface $taxon): void + public function iAddTaxonToTheProduct(ProductInterface $product, TaxonInterface $taxon): void { $this->updateSimpleProductPage->open(['id' => $product->getId()]); $this->updateSimpleProductPage->selectProductTaxon($taxon); diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php index 9e6a865655..513a504c37 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductVariantsContext.php @@ -107,6 +107,14 @@ final class ManagingProductVariantsContext implements Context $this->createPage->specifyPrice($price ?? '', $channel ?? $this->sharedStorage->get('channel')); } + /** + * @When /^I set its price to "-(?:€|£|\$)([^"]+)" for ("([^"]+)" channel)$/ + */ + public function iSetItsNegativePriceTo(string $price, ChannelInterface $channel): void + { + $this->createPage->specifyPrice('-' . $price, $channel); + } + /** * @When /^I set its minimum price to "(?:€|£|\$)([^"]+)" for ("([^"]+)" channel)$/ */ @@ -116,7 +124,7 @@ final class ManagingProductVariantsContext implements Context } /** - * @When I remove its price for :channel channel + * @When I remove its price from :channel channel */ public function iRemoveItsPriceForChannel(ChannelInterface $channel): void { @@ -150,7 +158,7 @@ final class ManagingProductVariantsContext implements Context /** * @When I choose :calculatorName calculator */ - public function iChooseCalculator($calculatorName) + public function iChooseCalculator(string $calculatorName): void { $this->createPage->choosePricingCalculator($calculatorName); } @@ -158,7 +166,7 @@ final class ManagingProductVariantsContext implements Context /** * @When I set its :optionName option to :optionValue */ - public function iSetItsOptionAs($optionName, $optionValue) + public function iSetItsOptionAs(string $optionName, string $optionValue): void { $this->createPage->selectOption($optionName, $optionValue); } @@ -180,9 +188,9 @@ final class ManagingProductVariantsContext implements Context } /** - * @When I do not want to have shipping required for this product + * @When I do not want to have shipping required for this product( variant) */ - public function iDoNotWantToHaveShippingRequiredForThisProduct() + public function iDoNotWantToHaveShippingRequiredForThisProduct(): void { $this->createPage->setShippingRequired(false); } @@ -203,6 +211,153 @@ final class ManagingProductVariantsContext implements Context $this->indexPage->bulkDelete(); } + /** + * @When /^I delete the ("[^"]+" variant of product "[^"]+")$/ + * @When /^I try to delete the ("[^"]+" variant of product "[^"]+")$/ + */ + public function iDeleteTheVariantOfProduct(ProductVariantInterface $productVariant): void + { + $this->indexPage->open(['productId' => $productVariant->getProduct()->getId()]); + + $this->indexPage->deleteResourceOnPage(['code' => $productVariant->getCode()]); + } + + /** + * @When /^I want to modify the ("[^"]+" product variant)$/ + */ + public function iWantToModifyAProduct(ProductVariantInterface $productVariant): void + { + $this->updatePage->open(['id' => $productVariant->getId(), 'productId' => $productVariant->getProduct()->getId()]); + } + + /** + * @When I choose to show this product in the :channelName channel + */ + public function iChooseToShowThisProductInTheChannel(string $channelName): void + { + $this->updatePage->showProductInChannel($channelName); + } + + /** + * @When I choose to show this product in this channel + */ + public function iChooseToShowThisProductInThisChannel(): void + { + $this->updatePage->showProductInSingleChannel(); + } + + /** + * @When I save my changes + * @When I try to save my changes + */ + public function iSaveMyChanges(): void + { + $this->updatePage->saveChanges(); + } + + /** + * @When I generate it + * @When I try to generate it + */ + public function iClickGenerate(): void + { + $this->generatePage->generate(); + } + + /** + * @When /^I specify that the (\d)(?:st|nd|rd|th) variant is identified by "([^"]+)" code and costs "(?:€|£|\$)([^"]+)" in (("[^"]+") channel)$/ + */ + public function iSpecifyThereAreVariantsIdentifiedByCodeWithCost( + int $nthVariant, + string $code, + int $price, + ChannelInterface $channel, + ): void { + $this->generatePage->specifyCode($nthVariant - 1, $code); + $this->generatePage->specifyPrice($nthVariant - 1, $price, $channel); + } + + /** + * @When /^I specify that the (\d)(?:st|nd|rd|th) variant is identified by "([^"]+)" code$/ + */ + public function iSpecifyThereAreVariantsIdentifiedByCode(int $nthVariant, string $code): void + { + $this->generatePage->specifyCode($nthVariant - 1, $code); + } + + /** + * @When /^I specify that the (\d)(?:st|nd|rd|th) variant costs "(?:€|£|\$)([^"]+)" in (("[^"]+") channel)$/ + */ + public function iSpecifyThereAreVariantsWithCost(int $nthVariant, int $price, ChannelInterface $channel): void + { + $this->generatePage->specifyPrice($nthVariant - 1, $price, $channel); + } + + /** + * @When /^I remove (\d)(?:st|nd|rd|th) variant from the list$/ + */ + public function iRemoveVariantFromTheList(int $nthVariant): void + { + $this->generatePage->removeVariant($nthVariant - 1); + } + + /** + * @When I set its shipping category as :shippingCategoryName + */ + public function iSetItsShippingCategoryAs(string $shippingCategoryName): void + { + $this->createPage->selectShippingCategory($shippingCategoryName); + } + + /** + * @When I do not specify any information about variants + */ + public function iDoNotSpecifyAnyInformationAboutVariants(): void + { + // Intentionally left blank to fulfill context expectation + } + + /** + * @When I change its quantity of inventory to :amount + */ + public function iChangeItsQuantityOfInventoryTo(int $amount): void + { + $this->updatePage->specifyCurrentStock($amount); + } + + /** + * @When /^I want to generate new variants for (this product)$/ + * @When /^I try to generate new variants for (this product)$/ + */ + public function iTryToGenerateNewVariantsForThisProduct(ProductInterface $product): void + { + $this->generatePage->open(['productId' => $product->getId()]); + } + + /** + * @When /^I disable it$/ + */ + public function iDisableIt(): void + { + $this->updatePage->disable(); + } + + /** + * @When /^I enable it$/ + */ + public function iEnableIt(): void + { + $this->updatePage->enable(); + } + + /** + * @When /^I change its price to "(?:€|£|\$)([^"]+)" for ("[^"]+" channel)$/ + */ + public function iChangeItsPriceToForChannel(int $originalPrice, ChannelInterface $channel): void + { + $this->updatePage->specifyPrice($originalPrice, $channel); + } + /** * @Then /^the (variant with code "[^"]+") should be priced at (?:€|£|\$)([^"]+) for (channel "([^"]+)")$/ * @Then /^the (variant with code "[^"]+") should be priced at "(?:€|£|\$)([^"]+)" for (channel "([^"]+)")$/ @@ -227,6 +382,7 @@ final class ManagingProductVariantsContext implements Context /** * @Then /^the (variant with code "[^"]+") should be originally priced at (?:€|£|\$)([^"]+) for (channel "[^"]+")$/ + * @Then /^the (variant with code "[^"]+") should be originally priced at "(?:€|£|\$)([^"]+)" for (channel "[^"]+")$/ */ public function theVariantWithCodeShouldBeOriginalPricedAtForChannel( ProductVariantInterface $productVariant, @@ -250,6 +406,7 @@ final class ManagingProductVariantsContext implements Context /** * @Then /^the (variant with code "[^"]+") should have an original price of (?:€|£|\$)([^"]+) for (channel "([^"]+)")$/ + * @Then /^the (variant with code "[^"]+") should have an original price of "(?:€|£|\$)([^"]+)" for (channel "([^"]+)")$/ */ public function theVariantWithCodeShouldHaveAnOriginalPriceOfForChannel(ProductVariantInterface $productVariant, $originalPrice, ChannelInterface $channel) { @@ -261,17 +418,6 @@ final class ManagingProductVariantsContext implements Context ); } - /** - * @When /^I delete the ("[^"]+" variant of product "[^"]+")$/ - * @When /^I try to delete the ("[^"]+" variant of product "[^"]+")$/ - */ - public function iDeleteTheVariantOfProduct(ProductVariantInterface $productVariant) - { - $this->indexPage->open(['productId' => $productVariant->getProduct()->getId()]); - - $this->indexPage->deleteResourceOnPage(['code' => $productVariant->getCode()]); - } - /** * @Then I should be notified that this variant is in use and cannot be deleted */ @@ -283,14 +429,6 @@ final class ManagingProductVariantsContext implements Context ); } - /** - * @When /^I want to modify the ("[^"]+" product variant)$/ - */ - public function iWantToModifyAProduct(ProductVariantInterface $productVariant) - { - $this->updatePage->open(['id' => $productVariant->getId(), 'productId' => $productVariant->getProduct()->getId()]); - } - /** * @Then the code field should be disabled */ @@ -374,7 +512,7 @@ final class ManagingProductVariantsContext implements Context { Assert::same( $this->generatePage->getPricesValidationMessage($position - 1), - 'You must define price for every channel.', + 'You must define price for every enabled channel.', ); } @@ -396,35 +534,10 @@ final class ManagingProductVariantsContext implements Context { Assert::contains( $this->createPage->getPricesValidationMessage(), - 'You must define price for every channel.', + 'You must define price for every enabled channel.', ); } - /** - * @When I choose to show this product in the :channel channel - */ - public function iChooseToShowThisProductInTheChannel(string $channel): void - { - $this->updatePage->showProductInChannel($channel); - } - - /** - * @When I choose to show this product in this channel - */ - public function iChooseToShowThisProductInThisChannel(): void - { - $this->updatePage->showProductInSingleChannel(); - } - - /** - * @When I save my changes - * @When I try to save my changes - */ - public function iSaveMyChanges() - { - $this->updatePage->saveChanges(); - } - /** * @Then /^inventory of (this variant) should not be tracked$/ */ @@ -445,48 +558,6 @@ final class ManagingProductVariantsContext implements Context Assert::true($this->updatePage->isTracked()); } - /** - * @When I generate it - * @When I try to generate it - */ - public function iClickGenerate() - { - $this->generatePage->generate(); - } - - /** - * @When /^I specify that the (\d)(?:st|nd|rd|th) variant is identified by "([^"]+)" code and costs "(?:€|£|\$)([^"]+)" in (("[^"]+") channel)$/ - */ - public function iSpecifyThereAreVariantsIdentifiedByCodeWithCost($nthVariant, $code, int $price, ChannelInterface $channel) - { - $this->generatePage->specifyCode($nthVariant - 1, $code); - $this->generatePage->specifyPrice($nthVariant - 1, $price, $channel); - } - - /** - * @When /^I specify that the (\d)(?:st|nd|rd|th) variant is identified by "([^"]+)" code$/ - */ - public function iSpecifyThereAreVariantsIdentifiedByCode($nthVariant, $code) - { - $this->generatePage->specifyCode($nthVariant - 1, $code); - } - - /** - * @When /^I specify that the (\d)(?:st|nd|rd|th) variant costs "(?:€|£|\$)([^"]+)" in (("[^"]+") channel)$/ - */ - public function iSpecifyThereAreVariantsWithCost($nthVariant, int $price, ChannelInterface $channel) - { - $this->generatePage->specifyPrice($nthVariant - 1, $price, $channel); - } - - /** - * @When /^I remove (\d)(?:st|nd|rd|th) variant from the list$/ - */ - public function iRemoveVariantFromTheList($nthVariant) - { - $this->generatePage->removeVariant($nthVariant - 1); - } - /** * @Then I should be notified that it has been successfully generated */ @@ -503,30 +574,6 @@ final class ManagingProductVariantsContext implements Context Assert::false($this->generatePage->isGenerationPossible()); } - /** - * @When I set its shipping category as :shippingCategoryName - */ - public function iSetItsShippingCategoryAs($shippingCategoryName) - { - $this->createPage->selectShippingCategory($shippingCategoryName); - } - - /** - * @When I do not specify any information about variants - */ - public function iDoNotSpecifyAnyInformationAboutVariants() - { - // Intentionally left blank to fulfill context expectation - } - - /** - * @When I change its quantity of inventory to :amount - */ - public function iChangeItsQuantityOfInventoryTo(int $amount) - { - $this->updatePage->specifyCurrentStock($amount); - } - /** * @Then /^the (variant with code "[^"]+") should not have shipping required$/ */ @@ -590,15 +637,6 @@ final class ManagingProductVariantsContext implements Context Assert::true($this->updatePage->isSelectedOptionValueOnPage($optionName, $valueName)); } - /** - * @When /^I want to generate new variants for (this product)$/ - * @When /^I try to generate new variants for (this product)$/ - */ - public function iTryToGenerateNewVariantsForThisProduct(ProductInterface $product): void - { - $this->generatePage->open(['productId' => $product->getId()]); - } - /** * @Then I should not be able to show this product in shop */ @@ -607,14 +645,6 @@ final class ManagingProductVariantsContext implements Context Assert::true($this->updatePage->isShowInShopButtonDisabled()); } - /** - * @When /^I disable it$/ - */ - public function iDisableIt(): void - { - $this->updatePage->disable(); - } - /** * @Then /^(this variant) should be disabled$/ */ @@ -625,14 +655,6 @@ final class ManagingProductVariantsContext implements Context Assert::false($this->updatePage->isEnabled()); } - /** - * @When /^I enable it$/ - */ - public function iEnableIt(): void - { - $this->updatePage->enable(); - } - /** * @Then /^(this variant) should be enabled$/ */ @@ -659,6 +681,19 @@ final class ManagingProductVariantsContext implements Context Assert::same($this->indexPage->countItemsWithNoName(), $count); } + /** + * @Then the variant :productVariant should have :optionName option as :optionValue + */ + public function theVariantShouldHaveOptionAs( + ProductVariantInterface $productVariant, + string $optionName, + string $optionValue, + ): void { + $this->updatePage->open(['id' => $productVariant->getId(), 'productId' => $productVariant->getProduct()->getId()]); + + Assert::true($this->updatePage->isSelectedOptionValueOnPage($optionName, $optionValue)); + } + /** * @param string $element * @param string $message diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductsContext.php index 0c17a02ced..579cecbc50 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingProductsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingProductsContext.php @@ -33,6 +33,7 @@ use Sylius\Behat\Service\Resolver\CurrentPageResolverInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Product\Model\ProductAssociationTypeInterface; use Webmozart\Assert\Assert; @@ -309,6 +310,7 @@ final class ManagingProductsContext implements Context /** * @Then the first product on the list should have :field :value + * @Then the first product on the list within this taxon should have :field :value */ public function theFirstProductOnTheListShouldHave($field, $value) { @@ -365,6 +367,7 @@ final class ManagingProductsContext implements Context /** * @Then the last product on the list should have :field :value + * @Then the last product on the list within this taxon should have :field :value */ public function theLastProductOnTheListShouldHave($field, $value) { @@ -399,6 +402,17 @@ final class ManagingProductsContext implements Context $this->indexPage->sortBy($field); } + /** + * @When I sort this taxon's products :sortType by :field + */ + public function iSortThisTaxonsProductsBy(string $sortType, string $field): void + { + $this->indexPerTaxonPage->sortBy( + $field, + str_starts_with($sortType, 'de') ? 'desc' : 'asc', + ); + } + /** * @Then I should see a single product in the list * @Then I should see :numberOfProducts products in the list @@ -518,6 +532,7 @@ final class ManagingProductsContext implements Context /** * @When I save my changes * @When I try to save my changes + * @When I save my changes to the images */ public function iSaveMyChanges() { @@ -561,14 +576,13 @@ final class ManagingProductsContext implements Context } /** - * @When I set its :attribute attribute to :value - * @When I set its :attribute attribute to :value in :language - * @When I do not set its :attribute attribute in :language - * @When I add the :attribute attribute + * @When I set its :attributeName attribute to :value in :localeCode + * @When I do not set its :attributeName attribute in :localeCode + * @When I add the :attributeName attribute */ - public function iSetItsAttributeTo($attribute, $value = null, $language = 'en_US') + public function iSetItsAttributeTo(string $attributeName, ?string $value = null, $localeCode = 'en_US'): void { - $this->createSimpleProductPage->addAttribute($attribute, $value ?? '', $language); + $this->createSimpleProductPage->addAttribute($attributeName, $value ?? '', $localeCode); } /** @@ -580,11 +594,19 @@ final class ManagingProductsContext implements Context } /** - * @When I set its non-translatable :attribute attribute to :value + * @When I select :value value for the :attribute attribute */ - public function iSetItsNonTranslatableAttributeTo(string $attribute, string $value): void + public function iSelectValueForTheAttribute(string $value, string $attribute): void { - $this->createSimpleProductPage->addNonTranslatableAttribute($attribute, $value); + $this->createSimpleProductPage->selectAttributeValue($attribute, $value, ''); + } + + /** + * @When I set its non-translatable :attributeName attribute to :value + */ + public function iSetItsNonTranslatableAttributeTo(string $attributeName, string $value): void + { + $this->createSimpleProductPage->addNonTranslatableAttribute($attributeName, $value); } /** @@ -624,13 +646,18 @@ final class ManagingProductsContext implements Context } /** - * @Then select attribute :attributeName of product :product should be :value in :language + * @Then select attribute :attributeName of product :product should be :value in :localeCode + * @Then select attribute :attributeName of product :product should be :value */ - public function itsSelectAttributeShouldBe($attributeName, ProductInterface $product, $value, $language = 'en_US') - { + public function itsSelectAttributeShouldBeIn( + string $attributeName, + ProductInterface $product, + string $value, + string $localeCode = '', + ): void { $this->updateSimpleProductPage->open(['id' => $product->getId()]); - Assert::same($this->updateSimpleProductPage->getAttributeSelectText($attributeName, $language), $value); + Assert::same($this->updateSimpleProductPage->getAttributeSelectText($attributeName, $localeCode), $value); } /** @@ -738,9 +765,9 @@ final class ManagingProductsContext implements Context } /** - * @Then the option field should be disabled + * @Then I should not be able to edit its options */ - public function theOptionFieldShouldBeDisabled() + public function iShouldNotBeAbleToEditItsOptions(): void { Assert::true($this->updateConfigurableProductPage->isProductOptionsDisabled()); } @@ -756,7 +783,7 @@ final class ManagingProductsContext implements Context } /** - * @Then I should see non-translatable attribute :attribute with value :value + * @Then I should see non-translatable attribute :attribute with value :value% */ public function iShouldSeeNonTranslatableAttributeWithValue(string $attribute, string $value): void { @@ -783,7 +810,18 @@ final class ManagingProductsContext implements Context $currentPage = $this->resolveCurrentPage(); $currentPage->open(['id' => $product->getId()]); - Assert::true($currentPage->isMainTaxonChosen($taxonName)); + Assert::true($currentPage->hasMainTaxonWithName($taxonName)); + } + + /** + * @Then the product :product should have the :taxon taxon + */ + public function thisProductTaxonShouldBe(ProductInterface $product, string $taxonName): void + { + $currentPage = $this->resolveCurrentPage(); + $currentPage->open(['id' => $product->getId()]); + + Assert::true($currentPage->isTaxonChosen($taxonName)); } /** @@ -809,14 +847,34 @@ final class ManagingProductsContext implements Context /** * @When I attach the :path image with :type type * @When I attach the :path image + * @When I attach the :path image with :type type to this product + * @When I attach the :path image to this product */ - public function iAttachImageWithType($path, $type = null) + public function iAttachImageWithType(string $path, ?string $type = null): void { $currentPage = $this->resolveCurrentPage(); $currentPage->attachImage($path, $type); } + /** + * @When I attach the :path image with selected :productVariant variant to this product + */ + public function iAttachImageWithSelectedVariantToThisProduct( + string $path, + ProductVariantInterface $productVariant, + ): void { + $this->updateConfigurableProductPage->attachImage(path: $path, productVariant: $productVariant); + } + + /** + * @When I select :productVariant variant for the first image + */ + public function iSelectVariantForTheFirstImage(ProductVariantInterface $productVariant): void + { + $this->updateConfigurableProductPage->selectVariantForFirstImage($productVariant); + } + /** * @When I associate as :productAssociationType the :productName product * @When I associate as :productAssociationType the :firstProductName and :secondProductName products @@ -876,6 +934,14 @@ final class ManagingProductsContext implements Context Assert::true($currentPage->isImageWithTypeDisplayed($type)); } + /** + * @Then its image should have :productVariant variant selected + */ + public function itsImageShouldHaveVariantSelected(ProductVariantInterface $productVariant): void + { + Assert::true($this->updateConfigurableProductPage->hasLastImageAVariant($productVariant)); + } + /** * @Then /^the (product "[^"]+") should still have an accessible image$/ */ @@ -1040,7 +1106,7 @@ final class ManagingProductsContext implements Context { Assert::same( $this->createSimpleProductPage->getChannelPricingValidationMessage(), - 'You must define price for every channel.', + 'You must define price for every enabled channel.', ); } @@ -1069,7 +1135,7 @@ final class ManagingProductsContext implements Context } /** - * @When /^I remove its price for ("[^"]+" channel)$/ + * @When /^I remove its price from ("[^"]+" channel)$/ */ public function iRemoveItsPriceForChannel(ChannelInterface $channel): void { @@ -1077,7 +1143,7 @@ final class ManagingProductsContext implements Context } /** - * @Then this product should( still) have slug :value in :language + * @Then this product should( still) have slug :value in :language (locale) */ public function thisProductElementShouldHaveSlugIn($slug, $language) { @@ -1381,7 +1447,7 @@ final class ManagingProductsContext implements Context Assert::same($currentPage->getValidationMessage($element), $message); } - private function resolveCurrentPage(): CreateConfigurableProductPageInterface|CreateSimpleProductPageInterface|\Sylius\Behat\Page\Admin\Product\IndexPageInterface|IndexPerTaxonPageInterface|UpdateConfigurableProductPageInterface|UpdateSimpleProductPageInterface + private function resolveCurrentPage(): CreateConfigurableProductPageInterface|CreateSimpleProductPageInterface|IndexPerTaxonPageInterface|\Sylius\Behat\Page\Admin\Product\IndexPageInterface|UpdateConfigurableProductPageInterface|UpdateSimpleProductPageInterface { return $this->currentPageResolver->getCurrentPageWithForm([ $this->indexPage, diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionCouponsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionCouponsContext.php index d4373f3208..c3977d7783 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionCouponsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionCouponsContext.php @@ -98,7 +98,7 @@ final class ManagingPromotionCouponsContext implements Context } /** - * @When /^I limit generated coupons usage to (\d+) times$/ + * @When /^I limit generated coupons usage to (\d+) times?$/ */ public function iSetGeneratedCouponsUsageLimitTo(int $limit) { @@ -123,7 +123,7 @@ final class ManagingPromotionCouponsContext implements Context } /** - * @When I limit its usage to :limit times + * @When I limit its usage to :limit time(s) */ public function iLimitItsUsageLimitTo(int $limit) { @@ -149,7 +149,7 @@ final class ManagingPromotionCouponsContext implements Context } /** - * @When /^I limit its per customer usage to ([^"]+) times$/ + * @When /^I limit its per customer usage to ([^"]+) times?$/ */ public function iLimitItsPerCustomerUsageLimitTo(int $limit) { @@ -164,6 +164,14 @@ final class ManagingPromotionCouponsContext implements Context $this->updatePage->setCustomerUsageLimit($limit); } + /** + * @When I make it not reusable from cancelled orders + */ + public function iMakeItReusableFromCancelledOrders(): void + { + $this->updatePage->toggleReusableFromCancelledOrders(false); + } + /** * @When I make it valid until :date */ @@ -173,9 +181,9 @@ final class ManagingPromotionCouponsContext implements Context } /** - * @When I change expires date to :date + * @When I change its expiration date to :date */ - public function iChangeExpiresDateTo(\DateTimeInterface $date) + public function iChangeItsExpirationDateTo(\DateTimeInterface $date) { $this->updatePage->setExpiresAt($date); } @@ -275,9 +283,7 @@ final class ManagingPromotionCouponsContext implements Context } /** - * @Then /^there should be (0|1) coupon related to (this promotion)$/ - * @Then /^there should be (\b(?![01]\b)\d{1,9}\b) coupons related to (this promotion)$/ - * @Then /^there should still be (\d+) coupons related to (this promotion)$/ + * @Then /^there should(?:| still) be (\d+) coupons? related to (this promotion)$/ */ public function thereShouldBeCouponRelatedTo(int $number, PromotionInterface $promotion): void { @@ -315,9 +321,10 @@ final class ManagingPromotionCouponsContext implements Context } /** - * @Then /^there should be coupon with code "([^"]+)"$/ + * @Then there should be a coupon with code :code + * @Then there should be a :promotion promotion with a coupon code :code */ - public function thereShouldBeCouponWithCode($code) + public function thereShouldBeCouponWithCode(string $code): void { Assert::true($this->indexPage->isSingleResourceOnPage(['code' => $code])); } @@ -325,9 +332,9 @@ final class ManagingPromotionCouponsContext implements Context /** * @Then this coupon should be valid until :date */ - public function thisCouponShouldBeValidUntil(\DateTimeInterface $date) + public function thisCouponShouldBeValidUntil(\DateTime $date) { - Assert::true($this->indexPage->isSingleResourceOnPage(['expiresAt' => date('d-m-Y', $date->getTimestamp())])); + Assert::true($this->indexPage->isSingleResourceOnPage(['expiresAt' => $date->format('d-m-Y')])); } /** @@ -355,9 +362,20 @@ final class ManagingPromotionCouponsContext implements Context } /** + * @Then /^(this coupon) should not be reusable from cancelled orders$/ + */ + public function thisCouponShouldBeReusableFromCancelledOrders(PromotionCouponInterface $coupon): void + { + $this->updatePage->open(['id' => $coupon->getId(), 'promotionId' => $coupon->getPromotion()->getId()]); + + Assert::false($this->updatePage->isReusableFromCancelledOrders()); + } + + /** + * @Then I should not be able to edit its code * @Then the code field should be disabled */ - public function theCodeFieldShouldBeDisabled() + public function iShouldNotBeAbleToEditItsCode(): void { Assert::true($this->updatePage->isCodeDisabled()); } diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php index e007ddb35d..98c0866296 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingPromotionsContext.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; +use Sylius\Behat\Element\Admin\Promotion\FormElementInterface; use Sylius\Behat\NotificationType; use Sylius\Behat\Page\Admin\Crud\IndexPageInterface as IndexPageCouponInterface; use Sylius\Behat\Page\Admin\Promotion\CreatePageInterface; @@ -36,6 +37,7 @@ final class ManagingPromotionsContext implements Context private UpdatePageInterface $updatePage, private CurrentPageResolverInterface $currentPageResolver, private NotificationCheckerInterface $notificationChecker, + private FormElementInterface $formElement, ) { } @@ -77,15 +79,15 @@ final class ManagingPromotionsContext implements Context } /** + * @When I set its priority to :priority * @When I remove its priority */ - public function iRemoveItsPriority() + public function iRemoveItsPriority(?int $priority = null): void { - $this->updatePage->setPriority(null); + $this->formElement->prioritizeIt($priority); } /** - * @Then I should see the promotion :promotionName in the list * @Then the :promotionName promotion should appear in the registry * @Then the :promotionName promotion should exist in the registry * @Then this promotion should still be named :promotionName @@ -108,10 +110,35 @@ final class ManagingPromotionsContext implements Context } /** - * @When I add the "Has at least one from taxons" rule configured with :firstTaxon - * @When I add the "Has at least one from taxons" rule configured with :firstTaxon and :secondTaxon + * @When I specify its label as :label in :localeCode locale */ - public function iAddTheHasTaxonRuleConfiguredWith(...$taxons) + public function iSpecifyItsLabelInLocaleCode(string $label, string $localeCode): void + { + $this->createPage->specifyLabel($label, $localeCode); + } + + /** + * @When I replace its label with a string exceeding the limit in :localeCode locale + */ + public function iSpecifyItsLabelWithAStringExceedingTheLimitInLocale(string $localeCode): void + { + $this->createPage->specifyLabel(str_repeat('a', 256), $localeCode); + } + + /** + * @When the :promotion promotion should have a label :label in :localeCode locale + */ + public function thePromotionShouldHaveLabelInLocale(PromotionInterface $promotion, string $label, string $localeCode): void + { + $this->updatePage->open(['id' => $promotion->getId()]); + $this->createPage->hasLabel($label, $localeCode); + } + + /** + * @When I add the "Has at least one from taxons" rule configured with :firstTaxon taxon + * @When I add the "Has at least one from taxons" rule configured with :firstTaxon taxon and :secondTaxon taxon + */ + public function iAddTheHasTaxonRuleConfiguredWith(string ...$taxons): void { $this->createPage->addRule('Has at least one from taxons'); @@ -119,7 +146,7 @@ final class ManagingPromotionsContext implements Context } /** - * @When /^I add the "Total price of items from taxon" rule configured with "([^"]+)" taxon and (?:€|£|\$)([^"]+) amount for ("[^"]+" channel)$/ + * @When /^I add the "Total price of items from taxon" rule configured with "([^"]+)" taxon and "(?:€|£|\$)([^"]+)" amount for ("[^"]+" channel)$/ */ public function iAddTheRuleConfiguredWith($taxonName, $amount, ChannelInterface $channel) { @@ -129,7 +156,7 @@ final class ManagingPromotionsContext implements Context } /** - * @When /^I add the "Item total" rule configured with (?:€|£|\$)([^"]+) amount for ("[^"]+" channel) and (?:€|£|\$)([^"]+) amount for ("[^"]+" channel)$/ + * @When /^I add the "Item total" rule configured with "(?:€|£|\$)([^"]+)" amount for ("[^"]+" channel) and "(?:€|£|\$)([^"]+)" amount for ("[^"]+" channel)$/ */ public function iAddTheItemTotalRuleConfiguredWithTwoChannel( $firstAmount, @@ -160,7 +187,7 @@ final class ManagingPromotionsContext implements Context } /** - * @When /^I specify that on ("[^"]+" channel) this action should be applied to items with price greater then "(?:€|£|\$)([^"]+)"$/ + * @When /^I specify that on ("[^"]+" channel) this action should be applied to items with price greater than "(?:€|£|\$)([^"]+)"$/ */ public function iAddAMinPriceFilterRangeForChannel(ChannelInterface $channel, $minimum) { @@ -168,7 +195,7 @@ final class ManagingPromotionsContext implements Context } /** - * @When /^I specify that on ("[^"]+" channel) this action should be applied to items with price lesser then "(?:€|£|\$)([^"]+)"$/ + * @When /^I specify that on ("[^"]+" channel) this action should be applied to items with price lesser than "(?:€|£|\$)([^"]+)"$/ */ public function iAddAMaxPriceFilterRangeForChannel(ChannelInterface $channel, $maximum) { @@ -193,7 +220,7 @@ final class ManagingPromotionsContext implements Context } /** - * @When /^I add the "([^"]+)" action configured with a percentage value of (?:|-)([^"]+)% for ("[^"]+" channel)$/ + * @When /^I add the "([^"]+)" action configured with a percentage value of "(?:|-)([^"]+)%" for ("[^"]+" channel)$/ */ public function iAddTheActionConfiguredWithAPercentageValueForChannel( string $actionType, @@ -205,7 +232,18 @@ final class ManagingPromotionsContext implements Context } /** - * @When /^I add the "([^"]+)" action configured with a percentage value of (?:|-)([^"]+)%$/ + * @When I add the :actionType action configured without a percentage value for :channel channel + */ + public function iAddTheActionConfiguredWithoutAPercentageValueForChannel( + string $actionType, + ChannelInterface $channel, + ): void { + $this->createPage->addAction($actionType); + $this->createPage->fillActionOptionForChannel($channel->getCode(), 'Percentage', ''); + } + + /** + * @When /^I add the "([^"]+)" action configured with a percentage value of "(?:|-)([^"]+)%"$/ * @When I add the :actionType action configured without a percentage value */ public function iAddTheActionConfiguredWithAPercentageValue($actionType, $percentage = null) @@ -239,6 +277,33 @@ final class ManagingPromotionsContext implements Context $this->indexPage->bulkDelete(); } + /** + * @When I archive the :promotionName promotion + */ + public function iArchiveThePromotion(string $promotionName): void + { + $actions = $this->indexPage->getActionsForResource(['name' => $promotionName]); + $actions->pressButton('Archive'); + } + + /** + * @When I restore the :promotionName promotion + */ + public function iRestoreThePromotion(string $promotionName): void + { + $actions = $this->indexPage->getActionsForResource(['name' => $promotionName]); + $actions->pressButton('Restore'); + } + + /** + * @When I filter archival promotions + */ + public function iFilterArchivalPromotions(): void + { + $this->indexPage->chooseArchival('Yes'); + $this->indexPage->filter(); + } + /** * @Then I should see a single promotion in the list * @Then there should be :amount promotions @@ -330,9 +395,9 @@ final class ManagingPromotionsContext implements Context } /** - * @When I make it exclusive + * @When I set it as exclusive */ - public function iMakeItExclusive() + public function iSetItAsExclusive(): void { /** @var CreatePageInterface|UpdatePageInterface $currentPage */ $currentPage = $this->currentPageResolver->getCurrentPageWithForm([$this->createPage, $this->updatePage]); @@ -428,9 +493,9 @@ final class ManagingPromotionsContext implements Context } /** - * @Then the code field should be disabled + * @Then I should not be able to edit its code */ - public function theCodeFieldShouldBeDisabled() + public function iShouldNotBeAbleToEditItsCode(): void { Assert::true($this->updatePage->isCodeDisabled()); } @@ -502,9 +567,9 @@ final class ManagingPromotionsContext implements Context } /** - * @Then I should be notified that promotion cannot end before it start + * @Then I should be notified that promotion cannot end before it starts */ - public function iShouldBeNotifiedThatPromotionCannotEndBeforeItsEvenStart() + public function iShouldBeNotifiedThatPromotionCannotEndBeforeItsEvenStarts(): void { /** @var CreatePageInterface|UpdatePageInterface $currentPage */ $currentPage = $this->currentPageResolver->getCurrentPageWithForm([$this->createPage, $this->updatePage]); @@ -776,6 +841,44 @@ final class ManagingPromotionsContext implements Context ); } + /** + * @Then I should be notified that promotion label in :localeCode locale is too long + */ + public function iShouldBeNotifiedThatPromotionLabelIsTooLong(string $localeCode): void + { + /** @var CreatePageInterface|UpdatePageInterface $currentPage */ + $currentPage = $this->currentPageResolver->getCurrentPageWithForm([$this->createPage, $this->updatePage]); + + Assert::same( + $currentPage->getValidationMessageForTranslation('label', $localeCode), + 'This value is too long. It should have 255 characters or less.', + ); + } + + /** + * @Then I should see the promotion :promotionName in the list + */ + public function iShouldSeeThePromotionInTheList(string $promotionName): void + { + Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $promotionName])); + } + + /** + * @Then I should not see the promotion :promotionName in the list + */ + public function iShouldNotSeeThePromotionInTheList(string $promotionName): void + { + Assert::false($this->indexPage->isSingleResourceOnPage(['name' => $promotionName])); + } + + /** + * @Then I should be viewing non archival promotions + */ + public function iShouldBeViewingNonArchivalPromotions(): void + { + Assert::false($this->indexPage->isArchivalFilterEnabled()); + } + private function assertFieldValidationMessage(string $element, string $expectedMessage) { /** @var CreatePageInterface|UpdatePageInterface $currentPage */ diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingShipmentsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingShipmentsContext.php index 425c87cf12..c5a3d71a70 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingShipmentsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingShipmentsContext.php @@ -172,6 +172,7 @@ final class ManagingShipmentsContext implements Context /** * @Then I should see order page with details of order :order + * @Then I should see the details of order :order */ public function iShouldSeeOrderPageWithDetailsOfOrder(OrderInterface $order): void { diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingShippingMethodsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingShippingMethodsContext.php index 73914acd94..b5b6022f4a 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingShippingMethodsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingShippingMethodsContext.php @@ -14,7 +14,6 @@ declare(strict_types=1); namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; -use Sylius\Behat\NotificationType; use Sylius\Behat\Page\Admin\ShippingMethod\CreatePageInterface; use Sylius\Behat\Page\Admin\ShippingMethod\IndexPageInterface; use Sylius\Behat\Page\Admin\ShippingMethod\UpdatePageInterface; @@ -147,6 +146,16 @@ final class ManagingShippingMethodsContext implements Context Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $shipmentMethodName])); } + /** + * @Then the shipping method :shipmentMethodName should not appear in the registry + */ + public function theShipmentMethodShouldNotAppearInTheRegistry(string $shipmentMethodName): void + { + $this->iWantToBrowseShippingMethods(); + + Assert::false($this->indexPage->isSingleResourceOnPage(['name' => $shipmentMethodName])); + } + /** * @Given /^(this shipping method) should still be in the registry$/ */ @@ -236,7 +245,7 @@ final class ManagingShippingMethodsContext implements Context /** * @Then I should be notified that code needs to contain only specific symbols */ - public function iShouldBeNotifiedThatCodeShouldContain() + public function iShouldBeNotifiedThatCodeNeedsToContainOnlySpecificSymbols(): void { $this->assertFieldValidationMessage( 'code', @@ -325,8 +334,9 @@ final class ManagingShippingMethodsContext implements Context /** * @Then I should be notified that :element has to be selected + * @Then I should be notified that the :element is required */ - public function iShouldBeNotifiedThatElementHasToBeSelected($element) + public function iShouldBeNotifiedThatElementHasToBeSelected(string $element): void { $this->assertFieldValidationMessage($element, sprintf('Please select shipping method %s.', $element)); } @@ -448,14 +458,6 @@ final class ManagingShippingMethodsContext implements Context Assert::false($this->indexPage->isSingleResourceOnPage(['code' => $shippingMethod->getCode()])); } - /** - * @Then I should be notified that it is in use - */ - public function iShouldBeNotifiedThatItIsInUse() - { - $this->notificationChecker->checkNotification('Cannot delete, the Shipping method is in use.', NotificationType::failure()); - } - /** * @Then I should be notified that amount for :channel channel should not be blank */ @@ -493,6 +495,15 @@ final class ManagingShippingMethodsContext implements Context $this->createPage->fillRuleOption('Weight', (string) $weight); } + /** + * @When I add the "Total weight greater than or equal" rule configured with invalid data + */ + public function iAddTheTotalWeightGreaterThanOrEqualRuleConfiguredWithInvalidData(): void + { + $this->createPage->addRule('Total weight greater than or equal'); + $this->createPage->fillRuleOption('Weight', 'invalid data'); + } + /** * @When I add the "Total weight less than or equal" rule configured with :weight */ @@ -503,21 +514,21 @@ final class ManagingShippingMethodsContext implements Context } /** - * @When /^I add the "Items total greater than or equal" rule configured with (?:€|£|\$)([^"]+) for ("[^"]+" channel)$/ + * @When /^I add the "([^"]+)" rule configured with (?:€|£|\$)([^"]+) for ("[^"]+" channel)$/ */ - public function iAddTheItemsTotalGreaterThanOrEqualRuleConfiguredWith($value, ChannelInterface $channel): void + public function iAddTheItemsTotalLessThanOrEqualRuleConfiguredWith(string $rule, mixed $value, ChannelInterface $channel): void { - $this->createPage->addRule('Items total greater than or equal'); + $this->createPage->addRule($rule); $this->createPage->fillRuleOptionForChannel($channel->getCode(), 'Amount', (string) $value); } /** - * @When /^I add the "Items total less than or equal" rule configured with (?:€|£|\$)([^"]+) for ("[^"]+" channel)$/ + * @When /^I add the "Items total less than or equal" rule configured with invalid data for ("[^"]+" channel)$/ */ - public function iAddTheItemsTotalLessThanOrEqualRuleConfiguredWith($value, ChannelInterface $channel): void + public function iAddTheItemsTotalLessThanOrEqualRuleConfiguredWithInvalidData(ChannelInterface $channel): void { $this->createPage->addRule('Items total less than or equal'); - $this->createPage->fillRuleOptionForChannel($channel->getCode(), 'Amount', (string) $value); + $this->createPage->fillRuleOptionForChannel($channel->getCode(), 'Amount', 'Invalid data'); } /** @@ -541,6 +552,28 @@ final class ManagingShippingMethodsContext implements Context ); } + /** + * @Then I should be notified that the weight rule has an invalid configuration + */ + public function iShouldBeNotifiedThatTheWeightRuleHasAnInvalidConfiguration(): void + { + $this->assertFieldValidationMessage('weight', 'Please enter a number.'); + } + + /** + * @Then I should be notified that the amount rule has an invalid configuration in :channel channel + */ + public function iShouldBeNotifiedThatTheAmountRuleHasAnInvalidConfigurationInChannel(ChannelInterface $channel): void + { + /** @var CreatePageInterface|UpdatePageInterface $currentPage */ + $currentPage = $this->currentPageResolver->getCurrentPageWithForm([$this->createPage, $this->updatePage]); + + Assert::same( + $currentPage->getValidationMessageForRuleAmount($channel->getCode()), + 'Please enter a valid money amount.', + ); + } + /** * @param string $element * @param string $expectedMessage diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingTaxRateContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingTaxRateContext.php index 5a4d776def..83bcac4ab0 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingTaxRateContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingTaxRateContext.php @@ -142,6 +142,19 @@ final class ManagingTaxRateContext implements Context Assert::true($this->indexPage->isSingleResourceOnPage(['name' => $taxRateName])); } + /** + * @Then the tax rate :taxRate should be included in price + */ + public function theTaxRateShouldIncludePrice(TaxRateInterface $taxRate): void + { + $this->updatePage->open(['id' => $taxRate->getId()]); + + Assert::true( + $taxRate->isIncludedInPrice(), + sprintf('Tax rate is not included in price'), + ); + } + /** * @Then I should not see a tax rate with name :name */ @@ -181,8 +194,9 @@ final class ManagingTaxRateContext implements Context /** * @Then the code field should be disabled + * @Then I should not be able to edit its code */ - public function theCodeFieldShouldBeDisabled() + public function iShouldNotBeAbleToEditItsCode(): void { Assert::true($this->updatePage->isCodeDisabled()); } diff --git a/src/Sylius/Behat/Context/Ui/Admin/ManagingTaxonsContext.php b/src/Sylius/Behat/Context/Ui/Admin/ManagingTaxonsContext.php index 3a193a1573..0bcee98cf1 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ManagingTaxonsContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ManagingTaxonsContext.php @@ -15,6 +15,7 @@ namespace Sylius\Behat\Context\Ui\Admin; use Behat\Behat\Context\Context; use Sylius\Behat\NotificationType; +use Sylius\Behat\Page\Admin\Product\UpdateSimpleProductPageInterface; use Sylius\Behat\Page\Admin\Taxon\CreateForParentPageInterface; use Sylius\Behat\Page\Admin\Taxon\CreatePageInterface; use Sylius\Behat\Page\Admin\Taxon\UpdatePageInterface; @@ -22,6 +23,7 @@ use Sylius\Behat\Service\Helper\JavaScriptTestHelper; use Sylius\Behat\Service\NotificationCheckerInterface; use Sylius\Behat\Service\Resolver\CurrentPageResolverInterface; use Sylius\Behat\Service\SharedStorageInterface; +use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\TaxonInterface; use Webmozart\Assert\Assert; @@ -35,6 +37,7 @@ final class ManagingTaxonsContext implements Context private CurrentPageResolverInterface $currentPageResolver, private NotificationCheckerInterface $notificationChecker, private JavaScriptTestHelper $testHelper, + private UpdateSimpleProductPageInterface $updateSimpleProductPage, ) { } @@ -154,6 +157,7 @@ final class ManagingTaxonsContext implements Context /** * @When I save my changes * @When I try to save my changes + * @When I save my changes to the images */ public function iSaveMyChanges() { @@ -190,9 +194,9 @@ final class ManagingTaxonsContext implements Context } /** - * @Then the code field should be disabled + * @Then I should not be able to edit its code */ - public function theCodeFieldShouldBeDisabled() + public function iShouldNotBeAbleToEditItsCode(): void { Assert::true($this->updatePage->isCodeDisabled()); } @@ -207,6 +211,16 @@ final class ManagingTaxonsContext implements Context Assert::same($this->updatePage->getSlug(), $slug); } + /** + * @Then the product :product should no longer have a main taxon + */ + public function theProductShouldNoLongerHaveAMainTaxon(ProductInterface $product): void + { + $this->updateSimpleProductPage->open(['id' => $product->getId()]); + + Assert::false($this->updateSimpleProductPage->hasMainTaxon()); + } + /** * @Then /^this taxon should (belongs to "[^"]+")$/ */ @@ -282,6 +296,19 @@ final class ManagingTaxonsContext implements Context Assert::same($this->createPage->countTaxons(), (int) $number); } + /** + * @When I attach the :path image with :type type + * @When I attach the :path image with :type type to this taxon + * @When I attach the :path image + * @When I attach the :path image to this taxon + */ + public function iAttachImageWithType(string $path, string $type = null): void + { + $currentPage = $this->resolveCurrentPage(); + + $currentPage->attachImage($path, $type); + } + /** * @Then I should see the taxon named :name in the list */ @@ -290,21 +317,10 @@ final class ManagingTaxonsContext implements Context Assert::same($this->createPage->countTaxonsByName($name), 1); } - /** - * @When I attach the :path image with :type type - * @When I attach the :path image - */ - public function iAttachImageWithType($path, $type = null) - { - $currentPage = $this->resolveCurrentPage(); - - $currentPage->attachImage($path, $type); - } - /** * @Then /^(?:it|this taxon) should(?:| also) have an image with "([^"]*)" type$/ */ - public function thisTaxonShouldHaveAnImageWithType($type) + public function thisTaxonShouldHaveAnImageWithType(string $type): void { Assert::true($this->updatePage->isImageWithTypeDisplayed($type)); } @@ -320,9 +336,9 @@ final class ManagingTaxonsContext implements Context /** * @When /^I(?:| also) remove an image with "([^"]*)" type$/ */ - public function iRemoveAnImageWithType($code) + public function iRemoveAnImageWithType(string $type): void { - $this->updatePage->removeImageWithType($code); + $this->updatePage->removeImageWithType($type); } /** @@ -456,7 +472,7 @@ final class ManagingTaxonsContext implements Context Assert::false($this->updatePage->isEnabled()); } - private function resolveCurrentPage(): CreateForParentPageInterface|CreatePageInterface|UpdatePageInterface + private function resolveCurrentPage(): CreateForParentPageInterface|CreatePageInterface|UpdateConfigurableProductPageInterface|UpdatePageInterface { return $this->currentPageResolver->getCurrentPageWithForm([ $this->createPage, diff --git a/src/Sylius/Behat/Context/Ui/Admin/NotificationContext.php b/src/Sylius/Behat/Context/Ui/Admin/NotificationContext.php index 0fd9a391e5..2ae6cc8be4 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/NotificationContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/NotificationContext.php @@ -40,6 +40,8 @@ final class NotificationContext implements Context /** * @Then I should be notified that it has been successfully edited + * @Then I should be notified that it has been successfully uploaded + * @Then I should be notified that the changes have been successfully applied */ public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void { @@ -76,14 +78,14 @@ final class NotificationContext implements Context } /** - * @Then I should be notified that it has been failed deleted :name + * @Then I should be notified that it is in use */ - public function iShouldBeNotifiedThatItHasBeenFailedDeleted(string $name): void + public function iShouldBeNotifiedThatItIsInUse(): void { $this->testHelper->waitUntilNotificationPopups( $this->notificationChecker, NotificationType::failure(), - 'Cannot delete, the ' . ucfirst($name) . ' is in use.', + 'Cannot delete', ); } } diff --git a/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php b/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php index da570f530a..b228ee9429 100644 --- a/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php +++ b/src/Sylius/Behat/Context/Ui/Admin/ProductShowPageContext.php @@ -60,9 +60,9 @@ final class ProductShowPageContext implements Context } /** - * @When I access :product product page + * @When I access the :product product */ - public function iAccessProductPage(ProductInterface $product): void + public function iAccessTheProduct(ProductInterface $product): void { $this->indexPage->showProductPage($product->getName()); } @@ -107,6 +107,24 @@ final class ProductShowPageContext implements Context $this->productShowPage->showVariantEditPage($variant); } + /** + * @When I access the price history of a simple product for :channelName channel + */ + public function iAccessThePriceHistoryIndexPageOfSimpleProductForChannel(string $channelName): void + { + $pricingRow = $this->pricingElement->getSimpleProductPricingRowForChannel($channelName); + $pricingRow->clickLink('Show'); + } + + /** + * @When I access the price history of a product variant :variantName for :channelName channel + */ + public function iAccessThePriceHistoryIndexPageOfVariantForChannel(string $variantName, string $channelName): void + { + $pricingRow = $this->pricingElement->getVariantPricingRowForChannel($variantName, $channelName); + $pricingRow->clickLink('Show'); + } + /** * @Then I should see this product's product page */ @@ -155,6 +173,53 @@ final class ProductShowPageContext implements Context Assert::same($this->pricingElement->getPriceForChannel($channelName), $price); } + /** + * @Then I should see :lowestPriceBeforeDiscount as its lowest price before the discount in :channelName channel + */ + public function iShouldSeeAsItsLowestPriceBeforeTheDiscountInChannel( + string $lowestPriceBeforeDiscount, + string $channelName, + ): void { + Assert::same($this->pricingElement->getLowestPriceBeforeDiscountForChannel($channelName), $lowestPriceBeforeDiscount); + } + + /** + * @Then I should not see the lowest price before the discount in :channelName channel + */ + public function iShouldNotSeeTheLowestPriceBeforeTheDiscountInChannel(string $channelName): void + { + Assert::same($this->pricingElement->getLowestPriceBeforeDiscountForChannel($channelName), '-'); + } + + /** + * @Then I should see the lowest price before the discount of :lowestPriceBeforeDiscount for :variantName variant in :channelName channel + */ + public function iShouldSeeVariantWithTheLowestPriceBeforeTheDiscountOfInChannel( + string $lowestPriceBeforeDiscount, + string $variantName, + string $channelName, + ): void { + Assert::true($this->variantsElement->hasProductVariantWithLowestPriceBeforeDiscountInChannel( + $variantName, + $lowestPriceBeforeDiscount, + $channelName, + )); + } + + /** + * @Then I should not see the lowest price before the discount for :variantName variant in :channelName channel + */ + public function iShouldNotSeeTheLowestPriceBeforeTheDiscountForVariantInChannel( + string $variantName, + string $channelName, + ): void { + Assert::true($this->variantsElement->hasProductVariantWithLowestPriceBeforeDiscountInChannel( + $variantName, + '-', + $channelName, + )); + } + /** * @Then I should not see price for channel :channelName */ @@ -220,9 +285,9 @@ final class ProductShowPageContext implements Context } /** - * @Then I should see product taxon is :taxonName + * @Then I should see product taxon :taxonName */ - public function iShouldSeeProductTaxonIs(string $taxonName): void + public function iShouldSeeProductTaxon(string $taxonName): void { Assert::true($this->taxonomyElement->hasProductTaxon($taxonName)); } @@ -320,7 +385,15 @@ final class ProductShowPageContext implements Context */ public function iShouldSeeProductAssociationWith(string $association, string $productName): void { - Assert::true($this->associationsElement->isProductAssociated($association, $productName)); + Assert::true($this->associationsElement->isAssociatedWith($association, $productName)); + } + + /** + * @Then I should see product association type :association + */ + public function iShouldSeeProductAssociationType(string $association): void + { + Assert::true($this->associationsElement->hasAssociation($association)); } /** @@ -358,6 +431,14 @@ final class ProductShowPageContext implements Context )); } + /** + * @Then I should see the :variantName variant + */ + public function iShouldSeeTheVariant(string $variantName): void + { + Assert::true($this->variantsElement->hasProductVariant($variantName)); + } + /** * @Then I should see attribute :attribute with value :value in :nameOfLocale locale */ @@ -367,7 +448,7 @@ final class ProductShowPageContext implements Context } /** - * @Then I should see non-translatable attribute :attribute with value :value + * @Then /^I should see non-translatable attribute "([^"]+)" with value ([^"]+)%$/ */ public function iShouldSeeNonTranslatableAttributeWithValue(string $attribute, string $value): void { diff --git a/src/Sylius/Behat/Context/Ui/EmailContext.php b/src/Sylius/Behat/Context/Ui/EmailContext.php index b04300e6b7..087809eacf 100644 --- a/src/Sylius/Behat/Context/Ui/EmailContext.php +++ b/src/Sylius/Behat/Context/Ui/EmailContext.php @@ -86,6 +86,28 @@ final class EmailContext implements Context ); } + /** + * @Then a verification email should have been sent to :recipient + */ + public function aVerificationEmailShouldHaveBeenSentTo(string $recipient): void + { + $this->assertEmailContainsMessageTo( + $this->translator->trans('sylius.email.user.account_verification.strategy'), + $recipient, + ); + } + + /** + * @Then a welcoming email should not have been sent to :recipient + */ + public function aWelcomingEmailShouldNotHaveBeenSentTo(string $recipient): void + { + $this->assertEmailDoesNotContainMessageTo( + $this->translator->trans('sylius.email.user_registration.welcome_to_our_store'), + $recipient, + ); + } + /** * @Then an email with the confirmation of the order :order should be sent to :email * @Then an email with the confirmation of the order :order should be sent to :email in :localeCode locale @@ -106,6 +128,25 @@ final class EmailContext implements Context ); } + /** + * @Then an email with the confirmation of the order :order should not be sent to :email + */ + public function anEmailWithTheConfirmationOfTheOrderShouldNotBeSentTo( + OrderInterface $order, + string $recipient, + string $localeCode = 'en_US', + ): void { + $this->assertEmailDoesNotContainMessageTo( + sprintf( + '%s %s %s', + $this->translator->trans('sylius.email.order_confirmation.your_order_number', [], null, $localeCode), + $order->getNumber(), + $this->translator->trans('sylius.email.order_confirmation.has_been_successfully_placed', [], null, $localeCode), + ), + $recipient, + ); + } + /** * @Then /^an email with the summary of (order placed by "[^"]+") should be sent to him$/ * @Then /^an email with the summary of (order placed by "[^"]+") should be sent to him in ("([^"]+)" locale)$/ @@ -166,11 +207,24 @@ final class EmailContext implements Context Assert::false($this->emailChecker->hasRecipient($recipient)); } + /** + * @Then only one email should have been sent to :recipient + */ + public function onlyOneEmailShouldHaveBeenSentTo(string $recipient): void + { + Assert::eq($this->emailChecker->countMessagesTo($recipient), 1); + } + private function assertEmailContainsMessageTo(string $message, string $recipient): void { Assert::true($this->emailChecker->hasMessageTo($message, $recipient)); } + private function assertEmailDoesNotContainMessageTo(string $message, string $recipient): void + { + Assert::false($this->emailChecker->hasMessageTo($message, $recipient)); + } + private function getShippingMethodName(OrderInterface $order): string { /** @var ShipmentInterface|false $shipment */ diff --git a/src/Sylius/Behat/Context/Ui/Shop/AccountContext.php b/src/Sylius/Behat/Context/Ui/Shop/AccountContext.php index b47cb8d87c..42f2c554fa 100644 --- a/src/Sylius/Behat/Context/Ui/Shop/AccountContext.php +++ b/src/Sylius/Behat/Context/Ui/Shop/AccountContext.php @@ -46,7 +46,7 @@ final class AccountContext implements Context /** * @When I want to modify my profile */ - public function iWantToModifyMyProfile() + public function iWantToModifyMyProfile(): void { $this->profileUpdatePage->open(); } @@ -55,7 +55,7 @@ final class AccountContext implements Context * @When I specify the first name as :firstName * @When I remove the first name */ - public function iSpecifyTheFirstName($firstName = null) + public function iSpecifyTheFirstName($firstName = null): void { $this->profileUpdatePage->specifyFirstName($firstName); } @@ -64,7 +64,7 @@ final class AccountContext implements Context * @When I specify the last name as :lastName * @When I remove the last name */ - public function iSpecifyTheLastName($lastName = null) + public function iSpecifyTheLastName($lastName = null): void { $this->profileUpdatePage->specifyLastName($lastName); } @@ -73,7 +73,7 @@ final class AccountContext implements Context * @When I specify the customer email as :email * @When I remove the customer email */ - public function iSpecifyCustomerTheEmail($email = null) + public function iSpecifyCustomerTheEmail($email = null): void { $this->profileUpdatePage->specifyEmail($email); } @@ -82,7 +82,7 @@ final class AccountContext implements Context * @When I save my changes * @When I try to save my changes */ - public function iSaveMyChanges() + public function iSaveMyChanges(): void { $this->profileUpdatePage->saveChanges(); } @@ -90,16 +90,24 @@ final class AccountContext implements Context /** * @Then I should be notified that it has been successfully edited */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited() + public function iShouldBeNotifiedThatItHasBeenSuccessfullyEdited(): void { $this->notificationChecker->checkNotification('has been successfully updated.', NotificationType::success()); } + /** + * @Then I should be notified that I can no longer change payment method of this order + */ + public function iShouldBeNotifiedThatICanNoLongerChangePaymentMethodOfThisOrder(): void + { + Assert::true($this->orderIndexPage->hasFlashMessage('You can no longer change payment method of this order')); + } + /** * @Then my name should be :name * @Then my name should still be :name */ - public function myNameShouldBe($name) + public function myNameShouldBe($name): void { $this->dashboardPage->open(); @@ -110,7 +118,7 @@ final class AccountContext implements Context * @Then my email should be :email * @Then my email should still be :email */ - public function myEmailShouldBe($email) + public function myEmailShouldBe($email): void { $this->dashboardPage->open(); @@ -120,7 +128,7 @@ final class AccountContext implements Context /** * @Then /^I should be notified that the (email|password|city|street|first name|last name) is required$/ */ - public function iShouldBeNotifiedThatElementIsRequired($element) + public function iShouldBeNotifiedThatElementIsRequired($element): void { Assert::true($this->profileUpdatePage->checkValidationMessageFor( StringInflector::nameToCode($element), @@ -131,7 +139,7 @@ final class AccountContext implements Context /** * @Then /^I should be notified that the (email) is invalid$/ */ - public function iShouldBeNotifiedThatElementIsInvalid($element) + public function iShouldBeNotifiedThatElementIsInvalid($element): void { Assert::true($this->profileUpdatePage->checkValidationMessageFor( StringInflector::nameToCode($element), @@ -142,7 +150,7 @@ final class AccountContext implements Context /** * @Then I should be notified that the email is already used */ - public function iShouldBeNotifiedThatTheEmailIsAlreadyUsed() + public function iShouldBeNotifiedThatTheEmailIsAlreadyUsed(): void { Assert::true($this->profileUpdatePage->checkValidationMessageFor('email', 'This email is already used.')); } @@ -150,7 +158,7 @@ final class AccountContext implements Context /** * @When /^I want to change my password$/ */ - public function iWantToChangeMyPassword() + public function iWantToChangeMyPassword(): void { $this->changePasswordPage->open(); } @@ -158,17 +166,30 @@ final class AccountContext implements Context /** * @Given I change password from :oldPassword to :newPassword */ - public function iChangePasswordTo($oldPassword, $newPassword) + public function iChangePasswordTo($oldPassword, $newPassword): void { $this->iSpecifyTheCurrentPasswordAs($oldPassword); $this->iSpecifyTheNewPasswordAs($newPassword); $this->iSpecifyTheConfirmationPasswordAs($newPassword); } + /** + * @Given I am changing this order's payment method + */ + public function iWantToChangeThisOrdersPaymentMethod(): void + { + $this->iBrowseMyOrders(); + + /** @var OrderInterface $order */ + $order = $this->sharedStorage->get('order'); + + $this->orderIndexPage->changePaymentMethod($order); + } + /** * @Then I should be notified that my password has been successfully changed */ - public function iShouldBeNotifiedThatMyPasswordHasBeenSuccessfullyChanged() + public function iShouldBeNotifiedThatMyPasswordHasBeenSuccessfullyChanged(): void { $this->notificationChecker->checkNotification('has been changed successfully!', NotificationType::success()); } @@ -176,7 +197,7 @@ final class AccountContext implements Context /** * @Given I specify the current password as :password */ - public function iSpecifyTheCurrentPasswordAs($password) + public function iSpecifyTheCurrentPasswordAs($password): void { $this->changePasswordPage->specifyCurrentPassword($password); } @@ -184,7 +205,7 @@ final class AccountContext implements Context /** * @Given I specify the new password as :password */ - public function iSpecifyTheNewPasswordAs($password) + public function iSpecifyTheNewPasswordAs($password): void { $this->changePasswordPage->specifyNewPassword($password); } @@ -192,7 +213,7 @@ final class AccountContext implements Context /** * @Given I confirm this password as :password */ - public function iSpecifyTheConfirmationPasswordAs($password) + public function iSpecifyTheConfirmationPasswordAs($password): void { $this->changePasswordPage->specifyConfirmationPassword($password); } @@ -200,7 +221,7 @@ final class AccountContext implements Context /** * @Then I should be notified that provided password is different than the current one */ - public function iShouldBeNotifiedThatProvidedPasswordIsDifferentThanTheCurrentOne() + public function iShouldBeNotifiedThatProvidedPasswordIsDifferentThanTheCurrentOne(): void { Assert::true($this->changePasswordPage->checkValidationMessageFor( 'current_password', @@ -211,7 +232,7 @@ final class AccountContext implements Context /** * @Then I should be notified that the entered passwords do not match */ - public function iShouldBeNotifiedThatTheEnteredPasswordsDoNotMatch() + public function iShouldBeNotifiedThatTheEnteredPasswordsDoNotMatch(): void { Assert::true($this->changePasswordPage->checkValidationMessageFor( 'new_password', @@ -222,7 +243,7 @@ final class AccountContext implements Context /** * @Then I should be notified that the password should be at least :length characters long */ - public function iShouldBeNotifiedThatThePasswordShouldBeAtLeastCharactersLong(int $length) + public function iShouldBeNotifiedThatThePasswordShouldBeAtLeastCharactersLong(int $length): void { Assert::true($this->changePasswordPage->checkValidationMessageFor( 'new_password', @@ -252,10 +273,19 @@ final class AccountContext implements Context $this->orderShowPage->pay(); } + /** + * @Then I try to change my payment method to :paymentMethod + */ + public function iChoosePaymentMethod(PaymentMethodInterface $paymentMethod): void + { + $this->orderShowPage->choosePaymentMethod($paymentMethod); + $this->orderShowPage->pay(); + } + /** * @Then I should see a single order in the list */ - public function iShouldSeeASingleOrderInTheList() + public function iShouldSeeASingleOrderInTheList(): void { Assert::same($this->orderIndexPage->countOrders(), 1); } @@ -263,7 +293,7 @@ final class AccountContext implements Context /** * @Then this order should have :order number */ - public function thisOrderShouldHaveNumber(OrderInterface $order) + public function thisOrderShouldHaveNumber(OrderInterface $order): void { Assert::true($this->orderIndexPage->isOrderWithNumberInTheList($order->getNumber())); } @@ -280,7 +310,7 @@ final class AccountContext implements Context /** * @When I am viewing the summary of my last order */ - public function iViewingTheSummaryOfMyLastOrder() + public function iViewingTheSummaryOfMyLastOrder(): void { $this->orderIndexPage->open(); $this->orderIndexPage->openLastOrderPage(); @@ -309,7 +339,7 @@ final class AccountContext implements Context /** * @Then I should see :customerName, :street, :postcode, :city, :countryName as shipping address */ - public function iShouldSeeAsShippingAddress($customerName, $street, $postcode, $city, $countryName) + public function iShouldSeeAsShippingAddress($customerName, $street, $postcode, $city, $countryName): void { Assert::true($this->orderShowPage->hasShippingAddress($customerName, $street, $postcode, $city, $countryName)); } @@ -317,7 +347,7 @@ final class AccountContext implements Context /** * @Then I should see :customerName, :street, :postcode, :city, :countryName as billing address */ - public function itShouldBeShippedTo($customerName, $street, $postcode, $city, $countryName) + public function itShouldBeShippedTo($customerName, $street, $postcode, $city, $countryName): void { Assert::true($this->orderShowPage->hasBillingAddress($customerName, $street, $postcode, $city, $countryName)); } @@ -325,7 +355,7 @@ final class AccountContext implements Context /** * @Then I should see :total as order's total */ - public function iShouldSeeAsOrderSTotal($total) + public function iShouldSeeAsOrderSTotal($total): void { Assert::same($this->orderShowPage->getTotal(), $total); } @@ -333,7 +363,7 @@ final class AccountContext implements Context /** * @Then I should see :itemsTotal as order's subtotal */ - public function iShouldSeeAsOrderSSubtotal($subtotal) + public function iShouldSeeAsOrderSSubtotal($subtotal): void { Assert::same($this->orderShowPage->getSubtotal(), $subtotal); } @@ -342,7 +372,7 @@ final class AccountContext implements Context * @Then I should see that I have to pay :paymentAmount for this order * @Then I should see :paymentTotal as payment total */ - public function iShouldSeeIHaveToPayForThisOrder($paymentAmount) + public function iShouldSeeIHaveToPayForThisOrder($paymentAmount): void { Assert::same($this->orderShowPage->getPaymentPrice(), $paymentAmount); } @@ -350,7 +380,7 @@ final class AccountContext implements Context /** * @Then I should see :numberOfItems items in the list */ - public function iShouldSeeItemsInTheList($numberOfItems) + public function iShouldSeeItemsInTheList($numberOfItems): void { Assert::same($this->orderShowPage->countItems(), (int) $numberOfItems); } @@ -358,7 +388,7 @@ final class AccountContext implements Context /** * @Then the product named :productName should be in the items list */ - public function theProductShouldBeInTheItemsList($productName) + public function theProductShouldBeInTheItemsList($productName): void { Assert::true($this->orderShowPage->isProductInTheList($productName)); } @@ -381,7 +411,7 @@ final class AccountContext implements Context /** * @Then I should see :itemPrice as item price */ - public function iShouldSeeAsItemPrice($itemPrice) + public function iShouldSeeAsItemPrice($itemPrice): void { Assert::same($this->orderShowPage->getItemPrice(), $itemPrice); } @@ -389,7 +419,7 @@ final class AccountContext implements Context /** * @When I subscribe to the newsletter */ - public function iSubscribeToTheNewsletter() + public function iSubscribeToTheNewsletter(): void { $this->profileUpdatePage->subscribeToTheNewsletter(); } @@ -397,7 +427,7 @@ final class AccountContext implements Context /** * @Then I should be subscribed to the newsletter */ - public function iShouldBeSubscribedToTheNewsletter() + public function iShouldBeSubscribedToTheNewsletter(): void { Assert::true($this->profileUpdatePage->isSubscribedToTheNewsletter()); } @@ -405,7 +435,7 @@ final class AccountContext implements Context /** * @Then I should see :provinceName as province in the shipping address */ - public function iShouldSeeAsProvinceInTheShippingAddress($provinceName) + public function iShouldSeeAsProvinceInTheShippingAddress($provinceName): void { Assert::true($this->orderShowPage->hasShippingProvinceName($provinceName)); } @@ -413,7 +443,7 @@ final class AccountContext implements Context /** * @Then I should see :provinceName as province in the billing address */ - public function iShouldSeeAsProvinceInTheBillingAddress($provinceName) + public function iShouldSeeAsProvinceInTheBillingAddress($provinceName): void { Assert::true($this->orderShowPage->hasBillingProvinceName($provinceName)); } @@ -421,7 +451,7 @@ final class AccountContext implements Context /** * @Then I should be redirected to my account dashboard */ - public function iShouldBeRedirectedToMyAccountDashboard() + public function iShouldBeRedirectedToMyAccountDashboard(): void { Assert::true($this->dashboardPage->isOpen(), 'User should be on the account panel dashboard page but they are not.'); } @@ -429,7 +459,7 @@ final class AccountContext implements Context /** * @When I want to log in */ - public function iWantToLogIn() + public function iWantToLogIn(): void { $this->loginPage->tryToOpen(); } diff --git a/src/Sylius/Behat/Context/Ui/Shop/CartContext.php b/src/Sylius/Behat/Context/Ui/Shop/CartContext.php index 56d393e52f..980234772d 100644 --- a/src/Sylius/Behat/Context/Ui/Shop/CartContext.php +++ b/src/Sylius/Behat/Context/Ui/Shop/CartContext.php @@ -67,7 +67,7 @@ final class CartContext implements Context * @When I update my cart * @When I try to update my cart */ - public function iUpdateMyCart() + public function iUpdateMyCart(): void { $this->summaryPage->updateCart(); } @@ -85,7 +85,7 @@ final class CartContext implements Context * @Then my cart should be cleared * @Then cart should be empty with no value */ - public function iShouldBeNotifiedThatMyCartIsEmpty() + public function iShouldBeNotifiedThatMyCartIsEmpty(): void { $this->summaryPage->open(); @@ -114,9 +114,10 @@ final class CartContext implements Context /** * @Given I change :productName quantity to :quantity + * @Given I change product :productName quantity to :quantity * @Given I change product :productName quantity to :quantity in my cart */ - public function iChangeQuantityTo($productName, $quantity) + public function iChangeQuantityTo(string $productName, string $quantity): void { $this->summaryPage->open(); $this->summaryPage->changeQuantity($productName, $quantity); @@ -128,7 +129,7 @@ final class CartContext implements Context * @Then the cart total should be :total * @Then their cart total should be :total */ - public function myCartTotalShouldBe($total) + public function myCartTotalShouldBe(string $total): void { $this->summaryPage->open(); @@ -138,7 +139,7 @@ final class CartContext implements Context /** * @Then the grand total value in base currency should be :total */ - public function myBaseCartTotalShouldBe($total) + public function myBaseCartTotalShouldBe(string $total): void { $this->summaryPage->open(); @@ -209,7 +210,7 @@ final class CartContext implements Context /** * @Then my discount should be :promotionsTotal */ - public function myDiscountShouldBe($promotionsTotal) + public function myDiscountShouldBe(string $promotionsTotal): void { $this->summaryPage->open(); @@ -219,7 +220,7 @@ final class CartContext implements Context /** * @Given /^there should be no shipping fee$/ */ - public function thereShouldBeNoShippingFee() + public function thereShouldBeNoShippingFee(): void { $this->summaryPage->open(); @@ -235,7 +236,7 @@ final class CartContext implements Context /** * @Given /^there should be no discount$/ */ - public function thereShouldBeNoDiscount() + public function thereShouldBeNoDiscount(): void { $this->summaryPage->open(); @@ -253,7 +254,7 @@ final class CartContext implements Context * @Then /^(its|theirs) subtotal price should be decreased by ("[^"]+")$/ * @Then /^(product "[^"]+") price should be decreased by ("[^"]+")$/ */ - public function itsPriceShouldBeDecreasedBy(ProductInterface $product, $amount) + public function itsPriceShouldBeDecreasedBy(ProductInterface $product, int $amount): void { $this->summaryPage->open(); @@ -267,7 +268,7 @@ final class CartContext implements Context /** * @Then /^(product "[^"]+") price should be discounted by ("[^"]+")$/ */ - public function itsPriceShouldBeDiscountedBy(ProductInterface $product, $amount) + public function itsPriceShouldBeDiscountedBy(ProductInterface $product, int $amount): void { $this->summaryPage->open(); @@ -281,7 +282,7 @@ final class CartContext implements Context /** * @Then /^(product "[^"]+") price should not be decreased$/ */ - public function productPriceShouldNotBeDecreased(ProductInterface $product) + public function productPriceShouldNotBeDecreased(ProductInterface $product): void { $this->summaryPage->open(); @@ -311,8 +312,10 @@ final class CartContext implements Context /** * @When /^I add (products "([^"]+)" and "([^"]+)") to the cart$/ * @When /^I add (products "([^"]+)", "([^"]+)" and "([^"]+)") to the cart$/ + * + * @param ProductInterface[] $products */ - public function iAddMultipleProductsToTheCart(array $products) + public function iAddMultipleProductsToTheCart(array $products): void { foreach ($products as $product) { $this->iAddProductToTheCart($product); @@ -321,6 +324,8 @@ final class CartContext implements Context /** * @When /^an anonymous user in another browser adds (products "([^"]+)" and "([^"]+)") to the cart$/ + * + * @param ProductInterface[] $products */ public function anonymousUserAddsMultipleProductsToTheCart(array $products): void { @@ -337,7 +342,7 @@ final class CartContext implements Context * @Given I have :variantName variant of product :product in the cart * @Given /^I have "([^"]+)" variant of (this product) in the cart$/ */ - public function iAddProductToTheCartSelectingVariant($variantName, ProductInterface $product) + public function iAddProductToTheCartSelectingVariant(string $variantName, ProductInterface $product): void { $this->productShowPage->open(['slug' => $product->getSlug()]); $this->productShowPage->addToCartWithVariant($variantName); @@ -355,17 +360,17 @@ final class CartContext implements Context /** * @When /^I add (\d+) of (them) to (?:the|my) cart$/ */ - public function iAddQuantityOfProductsToTheCart($quantity, ProductInterface $product) + public function iAddQuantityOfProductsToTheCart(string $quantity, ProductInterface $product): void { $this->productShowPage->open(['slug' => $product->getSlug()]); $this->productShowPage->addToCartWithQuantity($quantity); } /** - * @Given /^I have(?:| added) (\d+) (products "([^"]+)") (?:to|in) the cart$/ + * @Given /^I have(?:| added) (\d+) (product(?:|s) "([^"]+)") (?:to|in) the cart$/ * @When /^I add(?:|ed)(?:| again) (\d+) (products "([^"]+)") to the cart$/ */ - public function iAddProductsToTheCart($quantity, ProductInterface $product) + public function iAddProductsToTheCart(string $quantity, ProductInterface $product): void { $this->productShowPage->open(['slug' => $product->getSlug()]); $this->productShowPage->addToCartWithQuantity($quantity); @@ -385,7 +390,7 @@ final class CartContext implements Context * @Then /^I should be(?: on| redirected to) my cart summary page$/ * @Then I should not be able to address an order with an empty cart */ - public function shouldBeOnMyCartSummaryPage() + public function shouldBeOnMyCartSummaryPage(): void { $this->summaryPage->waitForRedirect(3); @@ -395,7 +400,7 @@ final class CartContext implements Context /** * @Then I should be notified that the product has been successfully added */ - public function iShouldBeNotifiedThatItHasBeenSuccessfullyAdded() + public function iShouldBeNotifiedThatItHasBeenSuccessfullyAdded(): void { $this->notificationChecker->checkNotification('Item has been added to cart', NotificationType::success()); } @@ -403,7 +408,7 @@ final class CartContext implements Context /** * @Then there should be one item in my cart */ - public function thereShouldBeOneItemInMyCart() + public function thereShouldBeOneItemInMyCart(): void { Assert::true($this->summaryPage->isSingleItemOnPage()); } @@ -411,7 +416,7 @@ final class CartContext implements Context /** * @Then this item should have name :itemName */ - public function thisProductShouldHaveName($itemName) + public function thisProductShouldHaveName(string $itemName): void { Assert::true($this->summaryPage->hasItemNamed($itemName)); } @@ -419,7 +424,7 @@ final class CartContext implements Context /** * @Then this item should have variant :variantName */ - public function thisItemShouldHaveVariant($variantName) + public function thisItemShouldHaveVariant(string $variantName): void { Assert::true($this->summaryPage->hasItemWithVariantNamed($variantName)); } @@ -427,7 +432,7 @@ final class CartContext implements Context /** * @Then this item should have code :variantCode */ - public function thisItemShouldHaveCode($variantCode) + public function thisItemShouldHaveCode(string $variantCode): void { Assert::true($this->summaryPage->hasItemWithCode($variantCode)); } @@ -460,7 +465,7 @@ final class CartContext implements Context /** * @Given /^(this product) should have ([^"]+) "([^"]+)"$/ */ - public function thisItemShouldHaveOptionValue(ProductInterface $product, $optionName, $optionValue) + public function thisItemShouldHaveOptionValue(ProductInterface $product, string $optionName, string $optionValue): void { Assert::true($this->summaryPage->hasItemWithOptionValue($product->getName(), $optionName, $optionValue)); } @@ -468,7 +473,7 @@ final class CartContext implements Context /** * @When I clear my cart */ - public function iClearMyCart() + public function iClearMyCart(): void { $this->summaryPage->clearCart(); } @@ -476,9 +481,9 @@ final class CartContext implements Context /** * @Then /^I should see "([^"]+)" with quantity (\d+) in my cart$/ */ - public function iShouldSeeWithQuantityInMyCart($productName, $quantity) + public function iShouldSeeWithQuantityInMyCart(string $productName, int $quantity): void { - Assert::same($this->summaryPage->getQuantity($productName), (int) $quantity); + Assert::same($this->summaryPage->getQuantity($productName), $quantity); } /** @@ -494,7 +499,7 @@ final class CartContext implements Context /** * @Then /^the product "([^"]+)" should have total price ("[^"]+") in the cart$/ */ - public function theProductShouldHaveTotalPrice(string $productName, int $totalPrice): void + public function theProductShouldHaveTotalPrice(string $productName, string $totalPrice): void { Assert::same($this->summaryPage->getItemTotal($productName), $totalPrice); } @@ -519,7 +524,7 @@ final class CartContext implements Context /** * @Given I use coupon with code :couponCode */ - public function iUseCouponWithCode($couponCode) + public function iUseCouponWithCode(string $couponCode): void { $this->summaryPage->applyCoupon($couponCode); } @@ -527,7 +532,7 @@ final class CartContext implements Context /** * @Then I should be notified that the coupon is invalid */ - public function iShouldBeNotifiedThatCouponIsInvalid() + public function iShouldBeNotifiedThatCouponIsInvalid(): void { Assert::same($this->summaryPage->getPromotionCouponValidationMessage(), 'Coupon code is invalid.'); } @@ -535,7 +540,7 @@ final class CartContext implements Context /** * @Then total price of :productName item should be :productPrice */ - public function thisItemPriceShouldBe($productName, $productPrice) + public function thisItemPriceShouldBe(string $productName, string $productPrice): void { $this->summaryPage->open(); @@ -545,15 +550,15 @@ final class CartContext implements Context /** * @Then /^I should be notified that (this product) has insufficient stock$/ */ - public function iShouldBeNotifiedThatThisProductDoesNotHaveSufficientStock(ProductInterface $product) + public function iShouldBeNotifiedThatThisProductDoesNotHaveSufficientStock(ProductInterface $product): void { - Assert::true($this->summaryPage->hasProductOutOfStockValidationMessage($product)); + Assert::true($this->summaryPage->hasItemWithInsufficientStock($product->getName())); } /** * @Then /^I should not be notified that (this product) cannot be updated$/ */ - public function iShouldNotBeNotifiedThatThisProductCannotBeUpdated(ProductInterface $product) + public function iShouldNotBeNotifiedThatThisProductCannotBeUpdated(ProductInterface $product): void { Assert::false($this->summaryPage->hasProductOutOfStockValidationMessage($product)); } @@ -561,7 +566,7 @@ final class CartContext implements Context /** * @Then my cart's total should be :total */ - public function myCartSTotalShouldBe($total) + public function myCartSTotalShouldBe(string $total): void { $this->summaryPage->open(); diff --git a/src/Sylius/Behat/Context/Ui/Shop/Checkout/CheckoutCompleteContext.php b/src/Sylius/Behat/Context/Ui/Shop/Checkout/CheckoutCompleteContext.php index 122d46d9f7..7fc3d024d4 100644 --- a/src/Sylius/Behat/Context/Ui/Shop/Checkout/CheckoutCompleteContext.php +++ b/src/Sylius/Behat/Context/Ui/Shop/Checkout/CheckoutCompleteContext.php @@ -18,6 +18,7 @@ use Behat\Mink\Exception\ElementNotFoundException; use FriendsOfBehat\PageObjectExtension\Page\UnexpectedPageException; use Sylius\Behat\NotificationType; use Sylius\Behat\Page\Shop\Checkout\CompletePageInterface; +use Sylius\Behat\Page\Shop\Order\ThankYouPageInterface; use Sylius\Behat\Service\NotificationCheckerInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Formatter\StringInflector; @@ -35,6 +36,7 @@ final class CheckoutCompleteContext implements Context private SharedStorageInterface $sharedStorage, private CompletePageInterface $completePage, private NotificationCheckerInterface $notificationChecker, + private ThankYouPageInterface $thankYouPage, ) { } @@ -145,7 +147,7 @@ final class CheckoutCompleteContext implements Context } /** - * @Then /^the ("[^"]+" product) should have unit price discounted by ("\$\d+")$/ + * @Then /^the ("[^"]+" product) should have unit price discounted by ("[^"]+")$/ */ public function theShouldHaveUnitPriceDiscountedFor(ProductInterface $product, int $amount): void { @@ -321,14 +323,15 @@ final class CheckoutCompleteContext implements Context } /** - * @Then I should be informed that order total has been changed + * @Then my order should not be placed due to changed order total */ - public function iShouldBeInformedThatOrderTotalHasBeenChanged() + public function myOrderShouldNotBePlacedDueToChangedOrderTotal(): void { $this->notificationChecker->checkNotification( 'Your order total has been changed, check your order information and confirm it again.', NotificationType::failure(), ); + Assert::false($this->thankYouPage->isOpen()); } /** diff --git a/src/Sylius/Behat/Context/Ui/Shop/Checkout/RegistrationAfterCheckoutContext.php b/src/Sylius/Behat/Context/Ui/Shop/Checkout/RegistrationAfterCheckoutContext.php index 8753dd1f6e..2230d671b0 100644 --- a/src/Sylius/Behat/Context/Ui/Shop/Checkout/RegistrationAfterCheckoutContext.php +++ b/src/Sylius/Behat/Context/Ui/Shop/Checkout/RegistrationAfterCheckoutContext.php @@ -15,12 +15,15 @@ namespace Sylius\Behat\Context\Ui\Shop\Checkout; use Behat\Behat\Context\Context; use Sylius\Behat\Element\Shop\Account\RegisterElementInterface; +use Sylius\Behat\Page\Shop\Account\DashboardPageInterface; use Sylius\Behat\Page\Shop\Account\LoginPageInterface; +use Sylius\Behat\Page\Shop\Account\RegisterThankYouPageInterface; use Sylius\Behat\Page\Shop\Account\VerificationPageInterface; use Sylius\Behat\Page\Shop\HomePageInterface; use Sylius\Behat\Page\Shop\Order\ThankYouPageInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\CustomerInterface; +use Sylius\Component\Core\Repository\CustomerRepositoryInterface; use Webmozart\Assert\Assert; final class RegistrationAfterCheckoutContext implements Context @@ -31,7 +34,10 @@ final class RegistrationAfterCheckoutContext implements Context private ThankYouPageInterface $thankYouPage, private HomePageInterface $homePage, private VerificationPageInterface $verificationPage, + private RegisterThankYouPageInterface $registerThankYouPage, + private DashboardPageInterface $dashboardPage, private RegisterElementInterface $registerElement, + private CustomerRepositoryInterface $customerRepository, ) { } @@ -93,4 +99,21 @@ final class RegistrationAfterCheckoutContext implements Context Assert::true($this->homePage->hasLogoutButton()); } + + /** + * @Then I should be on registration thank you page + */ + public function iShouldBeOnRegistrationThankYouPage(): void + { + $registeredCustomer = $this->customerRepository->findLatest(1)[0]; + Assert::true($this->registerThankYouPage->isOpen(['id' => $registeredCustomer->getId()])); + } + + /** + * @Then I should be on my account dashboard + */ + public function iShouldBeOnMyAccountDashboard(): void + { + Assert::true($this->dashboardPage->isOpen()); + } } diff --git a/src/Sylius/Behat/Context/Ui/Shop/CheckoutContext.php b/src/Sylius/Behat/Context/Ui/Shop/CheckoutContext.php index 883614b5e0..99710f60f0 100644 --- a/src/Sylius/Behat/Context/Ui/Shop/CheckoutContext.php +++ b/src/Sylius/Behat/Context/Ui/Shop/CheckoutContext.php @@ -91,6 +91,7 @@ final class CheckoutContext implements Context * @Given I have proceeded through checkout process in the :localeCode locale with email :email * @Given I have proceeded through checkout process * @When I proceed through checkout process + * @When I proceeded through checkout process * @When I proceed through checkout process in the :localeCode locale * @When I proceed through checkout process in the :localeCode locale with email :email */ diff --git a/src/Sylius/Behat/Context/Ui/Shop/LoginContext.php b/src/Sylius/Behat/Context/Ui/Shop/LoginContext.php index 06652d5bc6..cc5f34aad3 100644 --- a/src/Sylius/Behat/Context/Ui/Shop/LoginContext.php +++ b/src/Sylius/Behat/Context/Ui/Shop/LoginContext.php @@ -199,14 +199,6 @@ final class LoginContext implements Context Assert::true($this->loginPage->hasValidationErrorWith('Error Invalid credentials.')); } - /** - * @Then I should be notified about disabled account - */ - public function iShouldBeNotifiedAboutDisabledAccount(): void - { - Assert::true($this->loginPage->hasValidationErrorWith('Error Invalid credentials.')); - } - /** * @Then I should be notified that email with reset instruction has been sent */ @@ -285,6 +277,15 @@ final class LoginContext implements Context { $this->resetPasswordPage->tryToOpen(['token' => 'itotallyforgotmypassword']); + $this->iShouldNotBeAbleToChangeMyPasswordWithThisToken(); + } + + /** + * @Then I should not be able to change my password with this token + * @Then I should not be able to change my password + */ + public function iShouldNotBeAbleToChangeMyPasswordWithThisToken(): void + { Assert::false($this->resetPasswordPage->isOpen(), 'User should not be on the forgotten password page'); } } diff --git a/src/Sylius/Behat/Context/Ui/Shop/ProductContext.php b/src/Sylius/Behat/Context/Ui/Shop/ProductContext.php index b450c5afdc..3de1a35ac5 100644 --- a/src/Sylius/Behat/Context/Ui/Shop/ProductContext.php +++ b/src/Sylius/Behat/Context/Ui/Shop/ProductContext.php @@ -15,6 +15,7 @@ namespace Sylius\Behat\Context\Ui\Shop; use Behat\Behat\Context\Context; use Sylius\Behat\Element\Product\IndexPage\VerticalMenuElementInterface; +use Sylius\Behat\Element\Product\ShowPage\LowestPriceInformationElementInterface; use Sylius\Behat\Page\ErrorPageInterface; use Sylius\Behat\Page\Shop\Product\IndexPageInterface; use Sylius\Behat\Page\Shop\Product\ShowPageInterface; @@ -35,6 +36,7 @@ final class ProductContext implements Context private ErrorPageInterface $errorPage, private VerticalMenuElementInterface $verticalMenuElement, private ChannelContextSetterInterface $channelContextSetter, + private LowestPriceInformationElementInterface $lowestPriceInformationElement, ) { } @@ -258,30 +260,6 @@ final class ProductContext implements Context Assert::false($this->indexPage->isProductOnList($productName)); } - /** - * @Then I should see in taxon :taxon in the store products :firstProductName and :secondProductName - */ - public function iShouldSeeInTaxonInTheStoreProducts(TaxonInterface $taxon, string ...$productNames): void - { - $this->iCheckListOfProductsForTaxon($taxon); - - foreach ($productNames as $productName) { - Assert::true($this->indexPage->isProductOnList($productName)); - } - } - - /** - * @Then I should not see in taxon :taxon in the store products :firstProductName and :secondProductName - */ - public function iShouldNotSeeInTaxonInTheStoreProducts(TaxonInterface $taxon, string ...$productNames): void - { - $this->iCheckListOfProductsForTaxon($taxon); - - foreach ($productNames as $productName) { - Assert::false($this->indexPage->isProductOnList($productName)); - } - } - /** * @Then I should see empty list of products */ @@ -925,6 +903,22 @@ final class ProductContext implements Context Assert::false($this->showPage->hasBreadcrumbLink($taxonName)); } + /** + * @Then /^I should see "([^"]+)" as its lowest price before the discount$/ + */ + public function iShouldSeeAsItsLowestPriceBeforeTheDiscount(string $lowestPriceBeforeDiscount): void + { + Assert::true($this->lowestPriceInformationElement->isThereInformationAboutProductLowestPriceWithPrice($lowestPriceBeforeDiscount)); + } + + /** + * @Then I should not see information about its lowest price + */ + public function iShouldNotSeeInformationAboutItsLowestPrice(): void + { + Assert::false($this->lowestPriceInformationElement->isThereInformationAboutProductLowestPrice()); + } + /** * @param string $productName * @param string $productAssociationName diff --git a/src/Sylius/Behat/Context/Ui/Shop/RegistrationContext.php b/src/Sylius/Behat/Context/Ui/Shop/RegistrationContext.php index 002560a0cd..86745e5ad4 100644 --- a/src/Sylius/Behat/Context/Ui/Shop/RegistrationContext.php +++ b/src/Sylius/Behat/Context/Ui/Shop/RegistrationContext.php @@ -20,12 +20,14 @@ use Sylius\Behat\Page\Shop\Account\DashboardPageInterface; use Sylius\Behat\Page\Shop\Account\LoginPageInterface; use Sylius\Behat\Page\Shop\Account\ProfileUpdatePageInterface; use Sylius\Behat\Page\Shop\Account\RegisterPageInterface; +use Sylius\Behat\Page\Shop\Account\RegisterThankYouPageInterface; use Sylius\Behat\Page\Shop\Account\VerificationPageInterface; use Sylius\Behat\Page\Shop\HomePageInterface; use Sylius\Behat\Service\NotificationCheckerInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ShopUserInterface; +use Sylius\Component\Core\Repository\CustomerRepositoryInterface; use Webmozart\Assert\Assert; class RegistrationContext implements Context @@ -36,10 +38,12 @@ class RegistrationContext implements Context private HomePageInterface $homePage, private LoginPageInterface $loginPage, private RegisterPageInterface $registerPage, + private RegisterThankYouPageInterface $registerThankYouPage, private VerificationPageInterface $verificationPage, private ProfileUpdatePageInterface $profileUpdatePage, private RegisterElementInterface $registerElement, private NotificationCheckerInterface $notificationChecker, + private CustomerRepositoryInterface $customerRepository, ) { } @@ -348,6 +352,23 @@ class RegistrationContext implements Context Assert::true($this->profileUpdatePage->isSubscribedToTheNewsletter()); } + /** + * @Then I should be on registration thank you page + */ + public function iShouldBeOnRegistrationThankYouPage(): void + { + $registeredCustomer = $this->customerRepository->findLatest(1)[0]; + Assert::true($this->registerThankYouPage->isOpen(['id' => $registeredCustomer->getId()])); + } + + /** + * @Then I should be on my account dashboard + */ + public function iShouldBeOnMyAccountDashboard(): void + { + Assert::true($this->dashboardPage->isOpen()); + } + private function assertFieldValidationMessage(string $element, string $expectedMessage): void { Assert::true($this->registerElement->checkValidationMessageFor($element, $expectedMessage)); diff --git a/src/Sylius/Behat/Element/Admin/Channel/DiscountedProductsCheckingPeriodInputElement.php b/src/Sylius/Behat/Element/Admin/Channel/DiscountedProductsCheckingPeriodInputElement.php new file mode 100644 index 0000000000..3e1c960bbe --- /dev/null +++ b/src/Sylius/Behat/Element/Admin/Channel/DiscountedProductsCheckingPeriodInputElement.php @@ -0,0 +1,36 @@ +getElement('discounted_products_checking_period')->setValue($period); + } + + public function getPeriod(): int + { + return (int) $this->getElement('discounted_products_checking_period')->getValue(); + } + + protected function getDefinedElements(): array + { + return array_merge(parent::getDefinedElements(), [ + 'discounted_products_checking_period' => '#sylius_channel_channelPriceHistoryConfig_lowestPriceForDiscountedProductsCheckingPeriod', + ]); + } +} diff --git a/src/Sylius/Behat/Element/Admin/Channel/DiscountedProductsCheckingPeriodInputElementInterface.php b/src/Sylius/Behat/Element/Admin/Channel/DiscountedProductsCheckingPeriodInputElementInterface.php new file mode 100644 index 0000000000..a2d84d3e87 --- /dev/null +++ b/src/Sylius/Behat/Element/Admin/Channel/DiscountedProductsCheckingPeriodInputElementInterface.php @@ -0,0 +1,21 @@ +getElement('taxons_excluded_from_showing_lowest_price')->getParent(); + + AutocompleteHelper::chooseValue($this->getSession(), $excludeTaxonElement, $taxon->getName()); + } + + public function removeExcludedTaxon(TaxonInterface $taxon): void + { + $excludeTaxonElement = $this->getElement('taxons_excluded_from_showing_lowest_price')->getParent(); + + AutocompleteHelper::removeValue($this->getSession(), $excludeTaxonElement, $taxon->getName()); + } + + public function hasTaxonExcluded(TaxonInterface $taxon): bool + { + $excludedTaxons = $this->getElement('taxons_excluded_from_showing_lowest_price')->getValue(); + + return str_contains($excludedTaxons, $taxon->getCode()); + } + + protected function getDefinedElements(): array + { + return array_merge(parent::getDefinedElements(), [ + 'taxons_excluded_from_showing_lowest_price' => '#sylius_channel_channelPriceHistoryConfig_taxonsExcludedFromShowingLowestPrice', + ]); + } +} diff --git a/src/Sylius/Behat/Element/Admin/Channel/ExcludeTaxonsFromShowingLowestPriceInputElementInterface.php b/src/Sylius/Behat/Element/Admin/Channel/ExcludeTaxonsFromShowingLowestPriceInputElementInterface.php new file mode 100644 index 0000000000..06651abd80 --- /dev/null +++ b/src/Sylius/Behat/Element/Admin/Channel/ExcludeTaxonsFromShowingLowestPriceInputElementInterface.php @@ -0,0 +1,25 @@ +getElement('lowest_price_for_discounted_products_visible')->check(); + } + + public function disable(): void + { + $this->getElement('lowest_price_for_discounted_products_visible')->uncheck(); + } + + public function isEnabled(): bool + { + return $this->getElement('lowest_price_for_discounted_products_visible')->isChecked(); + } + + protected function getDefinedElements(): array + { + return [ + 'lowest_price_for_discounted_products_visible' => '#sylius_channel_channelPriceHistoryConfig_lowestPriceForDiscountedProductsVisible', + ]; + } +} diff --git a/src/Sylius/Behat/Element/Admin/Channel/LowestPriceFlagElementInterface.php b/src/Sylius/Behat/Element/Admin/Channel/LowestPriceFlagElementInterface.php new file mode 100644 index 0000000000..658589de50 --- /dev/null +++ b/src/Sylius/Behat/Element/Admin/Channel/LowestPriceFlagElementInterface.php @@ -0,0 +1,23 @@ +getElement('priority')->setValue($priority); + } + + protected function getDefinedElements(): array + { + return array_merge(parent::getDefinedElements(), [ + 'priority' => '#sylius_promotion_priority', + ]); + } +} diff --git a/src/Sylius/Behat/Element/Admin/Promotion/FormElementInterface.php b/src/Sylius/Behat/Element/Admin/Promotion/FormElementInterface.php new file mode 100644 index 0000000000..55c792c454 --- /dev/null +++ b/src/Sylius/Behat/Element/Admin/Promotion/FormElementInterface.php @@ -0,0 +1,19 @@ +getAssociatedProducts($this->getElement('associations'), $associationName); + } + + public function isAssociatedWith(string $associationName, string $productName): bool { - /** @var NodeElement $associations */ $associations = $this->getElement('associations'); /** @var NodeElement $product */ diff --git a/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElementInterface.php b/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElementInterface.php index bfd164f1c1..250eaf10ff 100644 --- a/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElementInterface.php +++ b/src/Sylius/Behat/Element/Product/ShowPage/AssociationsElementInterface.php @@ -15,5 +15,7 @@ namespace Sylius\Behat\Element\Product\ShowPage; interface AssociationsElementInterface { - public function isProductAssociated(string $associationName, string $productName): bool; + public function hasAssociation(string $associationName): bool; + + public function isAssociatedWith(string $associationName, string $productName): bool; } diff --git a/src/Sylius/Behat/Element/Product/ShowPage/AttributesElement.php b/src/Sylius/Behat/Element/Product/ShowPage/AttributesElement.php index e43e66b672..a364cbc74d 100644 --- a/src/Sylius/Behat/Element/Product/ShowPage/AttributesElement.php +++ b/src/Sylius/Behat/Element/Product/ShowPage/AttributesElement.php @@ -30,7 +30,7 @@ final class AttributesElement extends Element implements AttributesElementInterf { $attributeValue = $this->getDocument()->find('css', sprintf('.ui.segment[data-tab="non-translatable"] tr:contains("%s") td:nth-child(2)', $attribute))->getText(); - return $attributeValue === $value; + return str_contains($attributeValue, $value); } protected function getDefinedElements(): array diff --git a/src/Sylius/Behat/Element/Product/ShowPage/LowestPriceInformationElement.php b/src/Sylius/Behat/Element/Product/ShowPage/LowestPriceInformationElement.php new file mode 100644 index 0000000000..6c57430d3e --- /dev/null +++ b/src/Sylius/Behat/Element/Product/ShowPage/LowestPriceInformationElement.php @@ -0,0 +1,39 @@ +hasElement('lowest_price_information_element_with_price', [ + '%lowestPriceBeforeDiscount%' => $lowestPriceBeforeDiscount, + ]); + } + + public function isThereInformationAboutProductLowestPrice(): bool + { + return $this->hasElement('lowest_price_information_element') && $this->getElement('lowest_price_information_element')->isVisible(); + } + + protected function getDefinedElements(): array + { + return array_merge(parent::getDefinedElements(), [ + 'lowest_price_information_element' => '#lowest-price-before-discount:contains("The lowest price of this product from")', + 'lowest_price_information_element_with_price' => '#lowest-price-before-discount:contains("%lowestPriceBeforeDiscount%")', + ]); + } +} diff --git a/src/Sylius/Behat/Element/Product/ShowPage/LowestPriceInformationElementInterface.php b/src/Sylius/Behat/Element/Product/ShowPage/LowestPriceInformationElementInterface.php new file mode 100644 index 0000000000..e1d07b2e3e --- /dev/null +++ b/src/Sylius/Behat/Element/Product/ShowPage/LowestPriceInformationElementInterface.php @@ -0,0 +1,21 @@ + $element->getAttribute('href'), $appliedPromotions); } + public function getLowestPriceBeforeDiscountForChannel(string $channelName): string + { + $channelPriceRow = $this->getChannelPriceRow($channelName); + + if (null === $channelPriceRow) { + throw new \InvalidArgumentException(sprintf('Channel "%s" does not exist', $channelName)); + } + + $priceForChannel = $channelPriceRow->find('css', 'td:nth-child(4)'); + + return $priceForChannel->getText(); + } + + public function getSimpleProductPricingRowForChannel(string $channelName): NodeElement + { + return $this->getElement('simple_product_pricing_row', ['%channelName%' => $channelName]); + } + + public function getVariantPricingRowForChannel(string $variantName, string $channelName): NodeElement + { + return $this->getElement('variant_pricing_row', [ + '%variantName%' => $variantName, + '%channelName%' => $channelName, + ]); + } + + protected function getDefinedElements(): array + { + return array_merge(parent::getDefinedElements(), [ + 'simple_product_pricing_row' => '#pricing tr:contains("%channelName%")', + 'variant_pricing_row' => 'tr:contains("%variantName%") + tr:contains("%channelName%")', + ]); + } + private function getAppliedPromotionsForChannel(string $channelName): array { /** @var NodeElement $channelPriceRow */ diff --git a/src/Sylius/Behat/Element/Product/ShowPage/PricingElementInterface.php b/src/Sylius/Behat/Element/Product/ShowPage/PricingElementInterface.php index 4d61d4f877..17f1486897 100644 --- a/src/Sylius/Behat/Element/Product/ShowPage/PricingElementInterface.php +++ b/src/Sylius/Behat/Element/Product/ShowPage/PricingElementInterface.php @@ -13,6 +13,8 @@ declare(strict_types=1); namespace Sylius\Behat\Element\Product\ShowPage; +use Behat\Mink\Element\NodeElement; + interface PricingElementInterface { public function getPriceForChannel(string $channelName): string; @@ -22,4 +24,10 @@ interface PricingElementInterface public function getCatalogPromotionsNamesForChannel(string $channelName): array; public function getCatalogPromotionLinksForChannel(string $channelName): array; + + public function getLowestPriceBeforeDiscountForChannel(string $channelName): string; + + public function getSimpleProductPricingRowForChannel(string $channelName): NodeElement; + + public function getVariantPricingRowForChannel(string $variantName, string $channelName): NodeElement; } diff --git a/src/Sylius/Behat/Element/Product/ShowPage/VariantsElement.php b/src/Sylius/Behat/Element/Product/ShowPage/VariantsElement.php index cd2923c286..ef7540695e 100644 --- a/src/Sylius/Behat/Element/Product/ShowPage/VariantsElement.php +++ b/src/Sylius/Behat/Element/Product/ShowPage/VariantsElement.php @@ -26,6 +26,20 @@ final class VariantsElement extends Element implements VariantsElementInterface return \count($variants); } + public function hasProductVariant(string $name): bool + { + $variantRows = $this->getDocument()->findAll('css', '#variants .variants-accordion__title'); + + /** @var NodeElement $variant */ + foreach ($variantRows as $variant) { + if ($variant->find('css', '.content .variant-name')->getText() === $name) { + return true; + } + } + + return false; + } + public function hasProductVariantWithCodePriceAndCurrentStock( string $name, string $code, @@ -55,6 +69,29 @@ final class VariantsElement extends Element implements VariantsElementInterface return false; } + public function hasProductVariantWithLowestPriceBeforeDiscountInChannel( + string $productVariantName, + string $lowestPriceBeforeDiscount, + string $channelName, + ): bool { + /** @var NodeElement[] $variantRows */ + $variantRows = $this->getDocument()->findAll('css', '#variants .variants-accordion__title'); + + /** @var NodeElement $variant */ + foreach ($variantRows as $variant) { + if ($this->isVariantWithLowestPriceBeforeDiscountInChannel( + $variant, + $productVariantName, + $lowestPriceBeforeDiscount, + $channelName, + )) { + return true; + } + } + + return false; + } + private function hasProductWithGivenNameCodePriceAndCurrentStock( NodeElement $variant, string $name, @@ -65,10 +102,7 @@ final class VariantsElement extends Element implements VariantsElementInterface ): bool { $variantContent = $variant->getParent()->find( 'css', - sprintf( - '.variants-accordion__content.%s', - explode(' ', $variant->getAttribute('class'))[1], - ), + sprintf('.variants-accordion__content.%s', $this->getItemIndexClass($variant)), ); if ( @@ -82,4 +116,47 @@ final class VariantsElement extends Element implements VariantsElementInterface return false; } + + private function isVariantWithLowestPriceBeforeDiscountInChannel( + NodeElement $variant, + string $name, + string $lowestPriceBeforeDiscount, + string $channel, + ): bool { + $variantContent = $variant->getParent()->find( + 'css', + sprintf('.variants-accordion__content.%s', $this->getItemIndexClass($variant)), + ); + + $headerRow = $variantContent->find('css', '.pricing-header'); + $lowestPriceColumnIndex = $this->getLowestPriceColumnIndex($headerRow); + + if ($lowestPriceColumnIndex === null) { + return false; + } + + return + $variant->find('css', '.content .variant-name')->getText() === $name && + $variantContent->find( + 'css', + sprintf('tr.pricing:contains("%s") td:nth-child(%s)', $channel, $lowestPriceColumnIndex), + )->getText() === $lowestPriceBeforeDiscount + ; + } + + private function getItemIndexClass(NodeElement $variant): string + { + return explode(' ', $variant->getAttribute('class'))[1]; + } + + private function getLowestPriceColumnIndex(NodeElement $headerRow): ?int + { + foreach ($headerRow->findAll('css', 'td') as $index => $cell) { + if ($cell->getText() === 'Lowest price before discount') { + return $index + 1; + } + } + + return null; + } } diff --git a/src/Sylius/Behat/Element/Product/ShowPage/VariantsElementInterface.php b/src/Sylius/Behat/Element/Product/ShowPage/VariantsElementInterface.php index 58c79d1194..4d63247e10 100644 --- a/src/Sylius/Behat/Element/Product/ShowPage/VariantsElementInterface.php +++ b/src/Sylius/Behat/Element/Product/ShowPage/VariantsElementInterface.php @@ -17,6 +17,8 @@ interface VariantsElementInterface { public function countVariantsOnPage(): int; + public function hasProductVariant(string $name): bool; + public function hasProductVariantWithCodePriceAndCurrentStock( string $name, string $code, @@ -24,4 +26,10 @@ interface VariantsElementInterface string $currentStock, string $channel, ): bool; + + public function hasProductVariantWithLowestPriceBeforeDiscountInChannel( + string $productVariantName, + string $lowestPriceBeforeDiscount, + string $channelName, + ): bool; } diff --git a/src/Sylius/Behat/Element/Shop/MenuElement.php b/src/Sylius/Behat/Element/Shop/MenuElement.php index c02c1e33a3..d27dcd2422 100644 --- a/src/Sylius/Behat/Element/Shop/MenuElement.php +++ b/src/Sylius/Behat/Element/Shop/MenuElement.php @@ -22,7 +22,7 @@ final class MenuElement extends Element implements MenuElementInterface { $menu = $this->getElement('menu'); - return array_map(fn (NodeElement $element): string => $element->getText(), $menu->findAll('css', '[data-test-menu-item]')); + return array_map(fn (NodeElement $element): string => $element->getAttribute('data-test-menu-item'), $menu->findAll('css', '[data-test-menu-item]')); } protected function getDefinedElements(): array diff --git a/src/Sylius/Behat/Page/Admin/Channel/UpdatePage.php b/src/Sylius/Behat/Page/Admin/Channel/UpdatePage.php index 1ab49d35e9..6df7d5959a 100644 --- a/src/Sylius/Behat/Page/Admin/Channel/UpdatePage.php +++ b/src/Sylius/Behat/Page/Admin/Channel/UpdatePage.php @@ -132,6 +132,7 @@ class UpdatePage extends BaseUpdatePage implements UpdatePageInterface 'currencies' => '#sylius_channel_currencies', 'default_locale' => '#sylius_channel_defaultLocale', 'default_tax_zone' => '#sylius_channel_defaultTaxZone', + 'discounted_products_checking_period' => '#sylius_channel_channelPriceHistoryConfig_lowestPriceForDiscountedProductsCheckingPeriod', 'enabled' => '#sylius_channel_enabled', 'locales' => '#sylius_channel_locales', 'menu_taxon' => '#sylius_channel_menuTaxon ~ .text', diff --git a/src/Sylius/Behat/Page/Admin/ChannelPricingLogEntry/IndexPage.php b/src/Sylius/Behat/Page/Admin/ChannelPricingLogEntry/IndexPage.php new file mode 100644 index 0000000000..caed852f5c --- /dev/null +++ b/src/Sylius/Behat/Page/Admin/ChannelPricingLogEntry/IndexPage.php @@ -0,0 +1,50 @@ +getColumnFields('price'); + $availableOriginalPrices = $this->getColumnFields('originalPrice'); + $dates = $this->getColumnFields('loggedAt'); + + foreach ($availablePrices as $key => $value) { + Assert::notEmpty($dates[$key]); + + if ( + $availablePrices[$key] === $price && + $availableOriginalPrices[$key] === $originalPrice + ) { + return true; + } + } + + return false; + } + + public function isLogEntryWithPriceAndOriginalPriceOnPosition(string $price, string $originalPrice, int $position): bool + { + $availablePrices = $this->getColumnFields('price'); + $availableOriginalPrices = $this->getColumnFields('originalPrice'); + $dates = $this->getColumnFields('loggedAt'); + Assert::notEmpty($dates[$position - 1]); + + return $availablePrices[$position - 1] === $price && $availableOriginalPrices[$position - 1] === $originalPrice; + } +} diff --git a/src/Sylius/Behat/Page/Admin/ChannelPricingLogEntry/IndexPageInterface.php b/src/Sylius/Behat/Page/Admin/ChannelPricingLogEntry/IndexPageInterface.php new file mode 100644 index 0000000000..78a2d77919 --- /dev/null +++ b/src/Sylius/Behat/Page/Admin/ChannelPricingLogEntry/IndexPageInterface.php @@ -0,0 +1,23 @@ +tableAccessor->getRowsWithFields($this->getElement('table'), $parameters); return 1 === count($rows); - } catch (\InvalidArgumentException|ElementNotFoundException) { + } catch (ElementNotFoundException|\InvalidArgumentException) { return false; } } @@ -76,7 +76,7 @@ class IndexPage extends SymfonyPage implements IndexPageInterface } return null !== $rows[0]->find('css', $element); - } catch (\InvalidArgumentException|ElementNotFoundException) { + } catch (ElementNotFoundException|\InvalidArgumentException) { return false; } } diff --git a/src/Sylius/Behat/Page/Admin/Customer/IndexPage.php b/src/Sylius/Behat/Page/Admin/Customer/IndexPage.php index c2598693d2..1e6b4c791e 100644 --- a/src/Sylius/Behat/Page/Admin/Customer/IndexPage.php +++ b/src/Sylius/Behat/Page/Admin/Customer/IndexPage.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Behat\Page\Admin\Customer; use Sylius\Behat\Page\Admin\Crud\IndexPage as BaseIndexPage; +use Sylius\Behat\Service\AutocompleteHelper; use Sylius\Component\Customer\Model\CustomerInterface; class IndexPage extends BaseIndexPage implements IndexPageInterface @@ -37,4 +38,18 @@ class IndexPage extends BaseIndexPage implements IndexPageInterface return $tableAccessor->getFieldFromRow($table, $row, 'verified')->getText() === 'Yes'; } + + public function specifyFilterGroup(string $groupName): void + { + $groupFilterElement = $this->getElement('filter_group')->getParent(); + + AutocompleteHelper::chooseValue($this->getSession(), $groupFilterElement, $groupName); + } + + protected function getDefinedElements(): array + { + return array_merge(parent::getDefinedElements(), [ + 'filter_group' => '#criteria_group', + ]); + } } diff --git a/src/Sylius/Behat/Page/Admin/Customer/IndexPageInterface.php b/src/Sylius/Behat/Page/Admin/Customer/IndexPageInterface.php index 6bd3daa7be..a1ef8dd3e7 100644 --- a/src/Sylius/Behat/Page/Admin/Customer/IndexPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Customer/IndexPageInterface.php @@ -21,4 +21,6 @@ interface IndexPageInterface extends BaseIndexPageInterface public function getCustomerAccountStatus(CustomerInterface $customer): string; public function isCustomerVerified(CustomerInterface $customer): bool; + + public function specifyFilterGroup(string $groupName): void; } diff --git a/src/Sylius/Behat/Page/Admin/DashboardPage.php b/src/Sylius/Behat/Page/Admin/DashboardPage.php index b98e4c5b5b..c639bf66be 100644 --- a/src/Sylius/Behat/Page/Admin/DashboardPage.php +++ b/src/Sylius/Behat/Page/Admin/DashboardPage.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Page\Admin; +use Behat\Mink\Exception\ElementNotFoundException; use Behat\Mink\Session; use FriendsOfBehat\PageObjectExtension\Page\SymfonyPage; use Sylius\Behat\Service\Accessor\TableAccessorInterface; @@ -20,84 +21,133 @@ use Symfony\Component\Routing\RouterInterface; class DashboardPage extends SymfonyPage implements DashboardPageInterface { + /** + * @template TKey of array-key + * @template TValue + * + * @param array|\ArrayAccess $minkParameters + */ public function __construct( Session $session, - $minkParameters, + array|\ArrayAccess $minkParameters, RouterInterface $router, protected TableAccessorInterface $tableAccessor, ) { parent::__construct($session, $minkParameters, $router); } + /** @throws ElementNotFoundException */ public function getTotalSales(): string { return $this->getElement('total_sales')->getText(); } + /** @throws ElementNotFoundException */ public function getNumberOfNewOrders(): int { return (int) $this->getElement('new_orders')->getText(); } + /** @throws ElementNotFoundException */ public function getNumberOfNewOrdersInTheList(): int { return $this->tableAccessor->countTableBodyRows($this->getElement('order_list')); } + /** @throws ElementNotFoundException */ public function getNumberOfNewCustomers(): int { return (int) $this->getElement('new_customers')->getText(); } + /** @throws ElementNotFoundException */ public function getNumberOfNewCustomersInTheList(): int { return $this->tableAccessor->countTableBodyRows($this->getElement('customer_list')); } + /** @throws ElementNotFoundException */ public function getAverageOrderValue(): string { return $this->getElement('average_order_value')->getText(); } + /** @throws ElementNotFoundException */ public function getSubHeader(): string { return trim($this->getElement('sub_header')->getText()); } + /** @throws ElementNotFoundException */ public function isSectionWithLabelVisible(string $name): bool { return $this->getElement('admin_menu')->find('css', sprintf('div:contains(%s)', $name)) !== null; } + /** @throws ElementNotFoundException */ public function logOut(): void { $this->getElement('logout')->click(); } + /** @throws ElementNotFoundException */ public function chooseChannel(string $channelName): void { $this->getElement('channel_choosing_link', ['%channelName%' => $channelName])->click(); } + /** @throws ElementNotFoundException */ + public function chooseYearSplitByMonthsInterval(): void + { + $this->getElement('year_split_by_months_statistics_button')->click(); + } + + /** @throws ElementNotFoundException */ + public function chooseMonthSplitByDaysInterval(): void + { + $this->getElement('month_split_by_days_statistics_button')->click(); + } + + /** @throws ElementNotFoundException */ + public function choosePreviousPeriod(): void + { + $this->getElement('navigation_previous')->click(); + + usleep(500000); + } + + /** @throws ElementNotFoundException */ + public function chooseNextPeriod(): void + { + $this->getElement('navigation_next')->click(); + + usleep(500000); + } + public function getRouteName(): string { return 'sylius_admin_dashboard'; } + /** @return array */ protected function getDefinedElements(): array { return array_merge(parent::getDefinedElements(), [ + 'admin_menu' => '.sylius-admin-menu', 'average_order_value' => '#average-order-value', + 'channel_choosing_link' => 'a:contains("%channelName%")', 'customer_list' => '#customers', 'dropdown' => 'i.dropdown', 'logout' => '#sylius-logout-button', + 'month_split_by_days_statistics_button' => 'button[data-stats-button="month"]', + 'navigation_next' => '#navigation-next', + 'navigation_previous' => '#navigation-prev', 'new_customers' => '#new-customers', 'new_orders' => '#new-orders', 'order_list' => '#orders', - 'total_sales' => '#total-sales', 'sub_header' => '.ui.header .content .sub.header', - 'channel_choosing_link' => 'a:contains("%channelName%")', - 'admin_menu' => '.sylius-admin-menu', + 'total_sales' => '#total-sales', + 'year_split_by_months_statistics_button' => 'button[data-stats-button="year"]', ]); } } diff --git a/src/Sylius/Behat/Page/Admin/DashboardPageInterface.php b/src/Sylius/Behat/Page/Admin/DashboardPageInterface.php index 2172b62edc..24cd7f0156 100644 --- a/src/Sylius/Behat/Page/Admin/DashboardPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/DashboardPageInterface.php @@ -36,4 +36,12 @@ interface DashboardPageInterface extends SymfonyPageInterface public function logOut(): void; public function chooseChannel(string $channelName): void; + + public function chooseYearSplitByMonthsInterval(): void; + + public function chooseMonthSplitByDaysInterval(): void; + + public function choosePreviousPeriod(): void; + + public function chooseNextPeriod(): void; } diff --git a/src/Sylius/Behat/Page/Admin/Order/HistoryPage.php b/src/Sylius/Behat/Page/Admin/Order/HistoryPage.php index 1b54fa6c53..a694a6cf3f 100644 --- a/src/Sylius/Behat/Page/Admin/Order/HistoryPage.php +++ b/src/Sylius/Behat/Page/Admin/Order/HistoryPage.php @@ -26,4 +26,9 @@ class HistoryPage extends SymfonyPage implements HistoryPageInterface { return count($this->getDocument()->findAll('css', '#shipping-address-changes tbody tr')); } + + public function countBillingAddressChanges(): int + { + return count($this->getDocument()->findAll('css', '#billing-address-changes tbody tr')); + } } diff --git a/src/Sylius/Behat/Page/Admin/Order/HistoryPageInterface.php b/src/Sylius/Behat/Page/Admin/Order/HistoryPageInterface.php index 990077d5f6..10d174abab 100644 --- a/src/Sylius/Behat/Page/Admin/Order/HistoryPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Order/HistoryPageInterface.php @@ -18,4 +18,6 @@ use FriendsOfBehat\PageObjectExtension\Page\SymfonyPageInterface; interface HistoryPageInterface extends SymfonyPageInterface { public function countShippingAddressChanges(): int; + + public function countBillingAddressChanges(): int; } diff --git a/src/Sylius/Behat/Page/Admin/Order/ShowPage.php b/src/Sylius/Behat/Page/Admin/Order/ShowPage.php index 70709ddec8..991da2ff16 100644 --- a/src/Sylius/Behat/Page/Admin/Order/ShowPage.php +++ b/src/Sylius/Behat/Page/Admin/Order/ShowPage.php @@ -382,11 +382,21 @@ class ShowPage extends SymfonyPage implements ShowPageInterface $this->getElement('resend_order_confirmation_email')->click(); } + public function isResendOrderConfirmationEmailButtonVisible(): bool + { + return $this->getDocument()->has('css', '[data-test-resend-order-confirmation-email]'); + } + public function resendShipmentConfirmationEmail(): void { $this->getElement('resend_shipment_confirmation_email')->click(); } + public function isResendShipmentConfirmationEmailButtonVisible(): bool + { + return $this->getDocument()->has('css', '[data-test-resend-shipment-confirmation-email]'); + } + public function getShippedAtDate(): string { return $this->getElement('shipment_shipped_at_date')->getText(); diff --git a/src/Sylius/Behat/Page/Admin/Order/ShowPageInterface.php b/src/Sylius/Behat/Page/Admin/Order/ShowPageInterface.php index 53b04c37f2..9fbd74a36a 100644 --- a/src/Sylius/Behat/Page/Admin/Order/ShowPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Order/ShowPageInterface.php @@ -120,7 +120,11 @@ interface ShowPageInterface extends SymfonyPageInterface public function resendOrderConfirmationEmail(): void; + public function isResendOrderConfirmationEmailButtonVisible(): bool; + public function resendShipmentConfirmationEmail(): void; + public function isResendShipmentConfirmationEmailButtonVisible(): bool; + public function getShippedAtDate(): string; } diff --git a/src/Sylius/Behat/Page/Admin/PaymentMethod/CreatePage.php b/src/Sylius/Behat/Page/Admin/PaymentMethod/CreatePage.php index 293a31d308..ab484875ab 100644 --- a/src/Sylius/Behat/Page/Admin/PaymentMethod/CreatePage.php +++ b/src/Sylius/Behat/Page/Admin/PaymentMethod/CreatePage.php @@ -108,6 +108,8 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface 'gateway_name' => '#sylius_payment_method_gatewayConfig_gatewayName', 'name' => '#sylius_payment_method_translations_en_US_name', 'paypal_password' => '#sylius_payment_method_gatewayConfig_config_password', + 'stripe_secret_key' => '#sylius_payment_method_gatewayConfig_config_secret_key', + 'stripe_publishable_key' => '#sylius_payment_method_gatewayConfig_config_publishable_key', ]); } } diff --git a/src/Sylius/Behat/Page/Admin/Product/CreateConfigurableProductPage.php b/src/Sylius/Behat/Page/Admin/Product/CreateConfigurableProductPage.php index 1e204794c4..e8881f15a8 100644 --- a/src/Sylius/Behat/Page/Admin/Product/CreateConfigurableProductPage.php +++ b/src/Sylius/Behat/Page/Admin/Product/CreateConfigurableProductPage.php @@ -29,6 +29,7 @@ class CreateConfigurableProductPage extends BaseCreatePage implements CreateConf public function nameItIn(string $name, string $localeCode): void { $this->clickTabIfItsNotActive('details'); + $this->activateLanguageTab($localeCode); $this->getDocument()->fillField( sprintf('sylius_product_translations_%s_name', $localeCode), @@ -40,7 +41,7 @@ class CreateConfigurableProductPage extends BaseCreatePage implements CreateConf } } - public function isMainTaxonChosen(string $taxonName): bool + public function hasMainTaxonWithName(string $taxonName): bool { $this->openTaxonBookmarks(); $mainTaxonElement = $this->getElement('main_taxon')->getParent(); @@ -80,11 +81,34 @@ class CreateConfigurableProductPage extends BaseCreatePage implements CreateConf $imageForm->find('css', 'input[type="file"]')->attachFile($filesPath . $path); } + public function activateLanguageTab(string $localeCode): void + { + if (DriverHelper::isNotJavascript($this->getDriver())) { + return; + } + + $languageTabTitle = $this->getElement('language_tab', ['%localeCode%' => $localeCode]); + if (!$languageTabTitle->hasClass('active')) { + $languageTabTitle->click(); + } + } + + public function getAttributeValidationErrors(string $attributeName, string $localeCode): string + { + $this->clickTabIfItsNotActive('attributes'); + + $validationError = $this->getElement('attribute')->find('css', '.sylius-validation-error'); + + return $validationError->getText(); + } + protected function getDefinedElements(): array { return array_merge(parent::getDefinedElements(), [ + 'attribute' => '.attribute', 'code' => '#sylius_product_code', 'images' => '#sylius_product_images', + 'language_tab' => '[data-locale="%localeCode%"] .title', 'main_taxon' => '#sylius_product_mainTaxon', 'name' => '#sylius_product_translations_en_US_name', 'options_choice' => '#sylius_product_options', diff --git a/src/Sylius/Behat/Page/Admin/Product/CreateConfigurableProductPageInterface.php b/src/Sylius/Behat/Page/Admin/Product/CreateConfigurableProductPageInterface.php index 7b72f7f014..ea132c78d9 100644 --- a/src/Sylius/Behat/Page/Admin/Product/CreateConfigurableProductPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Product/CreateConfigurableProductPageInterface.php @@ -24,9 +24,13 @@ interface CreateConfigurableProductPageInterface extends BaseCreatePageInterface public function nameItIn(string $name, string $localeCode): void; - public function isMainTaxonChosen(string $taxonName): bool; + public function hasMainTaxonWithName(string $taxonName): bool; public function selectMainTaxon(TaxonInterface $taxon): void; public function attachImage(string $path, ?string $type = null): void; + + public function activateLanguageTab(string $localeCode): void; + + public function getAttributeValidationErrors(string $attributeName, string $localeCode): string; } diff --git a/src/Sylius/Behat/Page/Admin/Product/CreateSimpleProductPage.php b/src/Sylius/Behat/Page/Admin/Product/CreateSimpleProductPage.php index e7eb8eb59b..53546e3040 100644 --- a/src/Sylius/Behat/Page/Admin/Product/CreateSimpleProductPage.php +++ b/src/Sylius/Behat/Page/Admin/Product/CreateSimpleProductPage.php @@ -130,7 +130,7 @@ class CreateSimpleProductPage extends BaseCreatePage implements CreateSimpleProd AutocompleteHelper::chooseValue($this->getSession(), $mainTaxonElement, $taxon->getName()); } - public function isMainTaxonChosen(string $taxonName): bool + public function hasMainTaxonWithName(string $taxonName): bool { $this->openTaxonBookmarks(); $mainTaxonElement = $this->getElement('main_taxon')->getParent(); diff --git a/src/Sylius/Behat/Page/Admin/Product/CreateSimpleProductPageInterface.php b/src/Sylius/Behat/Page/Admin/Product/CreateSimpleProductPageInterface.php index c0ec52acea..dfd9e1ff8c 100644 --- a/src/Sylius/Behat/Page/Admin/Product/CreateSimpleProductPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Product/CreateSimpleProductPageInterface.php @@ -44,7 +44,7 @@ interface CreateSimpleProductPageInterface extends BaseCreatePageInterface public function removeAttribute(string $attributeName, string $localeCode): void; - public function isMainTaxonChosen(string $taxonName): bool; + public function hasMainTaxonWithName(string $taxonName): bool; public function selectMainTaxon(TaxonInterface $taxon): void; diff --git a/src/Sylius/Behat/Page/Admin/Product/UpdateConfigurableProductPage.php b/src/Sylius/Behat/Page/Admin/Product/UpdateConfigurableProductPage.php index b25f133a73..31a5542cba 100644 --- a/src/Sylius/Behat/Page/Admin/Product/UpdateConfigurableProductPage.php +++ b/src/Sylius/Behat/Page/Admin/Product/UpdateConfigurableProductPage.php @@ -17,6 +17,7 @@ use Behat\Mink\Element\NodeElement; use Sylius\Behat\Behaviour\ChecksCodeImmutability; use Sylius\Behat\Page\Admin\Crud\UpdatePage as BaseUpdatePage; use Sylius\Behat\Service\AutocompleteHelper; +use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Taxonomy\Model\TaxonInterface; use Webmozart\Assert\Assert; @@ -62,7 +63,7 @@ class UpdateConfigurableProductPage extends BaseUpdatePage implements UpdateConf return 'disabled' === $this->getElement('options')->getAttribute('disabled'); } - public function isMainTaxonChosen(string $taxonName): bool + public function hasMainTaxonWithName(string $taxonName): bool { $this->openTaxonBookmarks(); $mainTaxonElement = $this->getElement('main_taxon')->getParent(); @@ -102,19 +103,31 @@ class UpdateConfigurableProductPage extends BaseUpdatePage implements UpdateConf return in_array($statusCode, [200, 304], true); } - public function attachImage(string $path, string $type = null): void + public function hasLastImageAVariant(ProductVariantInterface $productVariant): bool { $this->clickTabIfItsNotActive('media'); - $filesPath = $this->getParameter('files_path'); + $imageForm = $this->getLastImageElement(); + return $productVariant->getCode() === $imageForm->find('css', 'input[type="hidden"]')->getValue(); + } + + public function attachImage(string $path, string $type = null, ?ProductVariantInterface $productVariant = null): void + { + $this->clickTabIfItsNotActive('media'); $this->getDocument()->clickLink('Add'); $imageForm = $this->getLastImageElement(); + if (null !== $type) { $imageForm->fillField('Type', $type); } + if (null !== $productVariant) { + $imageForm->find('css', 'input[type="hidden"]')->setValue($productVariant->getCode()); + } + + $filesPath = $this->getParameter('files_path'); $imageForm->find('css', 'input[type="file"]')->attachFile($filesPath . $path); } @@ -164,6 +177,14 @@ class UpdateConfigurableProductPage extends BaseUpdatePage implements UpdateConf $this->setImageType($firstImage, $type); } + public function selectVariantForFirstImage(ProductVariantInterface $productVariant): void + { + $this->clickTabIfItsNotActive('media'); + + $imageElement = $this->getFirstImageElement(); + $imageElement->find('css', 'input[type="hidden"]')->setValue($productVariant->getCode()); + } + public function countImages(): int { $imageElements = $this->getImageElements(); diff --git a/src/Sylius/Behat/Page/Admin/Product/UpdateConfigurableProductPageInterface.php b/src/Sylius/Behat/Page/Admin/Product/UpdateConfigurableProductPageInterface.php index e56913601b..5b50b61d93 100644 --- a/src/Sylius/Behat/Page/Admin/Product/UpdateConfigurableProductPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Product/UpdateConfigurableProductPageInterface.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Behat\Page\Admin\Product; use Sylius\Behat\Page\Admin\Crud\UpdatePageInterface; +use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Taxonomy\Model\TaxonInterface; interface UpdateConfigurableProductPageInterface extends UpdatePageInterface @@ -30,7 +31,7 @@ interface UpdateConfigurableProductPageInterface extends UpdatePageInterface public function isProductOptionsDisabled(): bool; - public function isMainTaxonChosen(string $taxonName): bool; + public function hasMainTaxonWithName(string $taxonName): bool; public function selectMainTaxon(TaxonInterface $taxon): void; @@ -38,7 +39,9 @@ interface UpdateConfigurableProductPageInterface extends UpdatePageInterface public function isImageWithTypeDisplayed(string $type): bool; - public function attachImage(string $path, string $type = null): void; + public function hasLastImageAVariant(ProductVariantInterface $productVariant): bool; + + public function attachImage(string $path, string $type = null, ?ProductVariantInterface $productVariant = null): void; public function changeImageWithType(string $type, string $path): void; @@ -48,6 +51,8 @@ interface UpdateConfigurableProductPageInterface extends UpdatePageInterface public function modifyFirstImageType(string $type): void; + public function selectVariantForFirstImage(ProductVariantInterface $productVariant): void; + public function countImages(): int; public function goToVariantsList(): void; diff --git a/src/Sylius/Behat/Page/Admin/Product/UpdateSimpleProductPage.php b/src/Sylius/Behat/Page/Admin/Product/UpdateSimpleProductPage.php index 743a4b0d77..ccfffea578 100644 --- a/src/Sylius/Behat/Page/Admin/Product/UpdateSimpleProductPage.php +++ b/src/Sylius/Behat/Page/Admin/Product/UpdateSimpleProductPage.php @@ -167,7 +167,14 @@ class UpdateSimpleProductPage extends BaseUpdatePage implements UpdateSimpleProd $productTaxonsElement->setValue(implode(',', $productTaxonsCodes)); } - public function isMainTaxonChosen(string $taxonName): bool + public function hasMainTaxon(): bool + { + $this->openTaxonBookmarks(); + + return $this->getDocument()->find('css', '.search > .text')->getText() !== ''; + } + + public function hasMainTaxonWithName(string $taxonName): bool { $this->openTaxonBookmarks(); $mainTaxonElement = $this->getElement('main_taxon')->getParent(); @@ -175,6 +182,15 @@ class UpdateSimpleProductPage extends BaseUpdatePage implements UpdateSimpleProd return $taxonName === $mainTaxonElement->find('css', '.search > .text')->getText(); } + public function isTaxonChosen(string $taxonName): bool + { + $productTaxonsElement = $this->getElement('product_taxons'); + + $taxonName = strtolower(str_replace('-', '_', $taxonName)); + + return str_contains($productTaxonsElement->getValue(), $taxonName); + } + public function disableTracking(): void { $this->getElement('tracked')->uncheck(); diff --git a/src/Sylius/Behat/Page/Admin/Product/UpdateSimpleProductPageInterface.php b/src/Sylius/Behat/Page/Admin/Product/UpdateSimpleProductPageInterface.php index ba573b707f..e4cc1159ff 100644 --- a/src/Sylius/Behat/Page/Admin/Product/UpdateSimpleProductPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Product/UpdateSimpleProductPageInterface.php @@ -47,7 +47,11 @@ interface UpdateSimpleProductPageInterface extends BaseUpdatePageInterface public function hasNonTranslatableAttributeWithValue(string $attributeName, string $value): bool; - public function isMainTaxonChosen(string $taxonName): bool; + public function hasMainTaxon(): bool; + + public function hasMainTaxonWithName(string $taxonName): bool; + + public function isTaxonChosen(string $taxonName): bool; public function selectMainTaxon(TaxonInterface $taxon): void; diff --git a/src/Sylius/Behat/Page/Admin/ProductReview/IndexPage.php b/src/Sylius/Behat/Page/Admin/ProductReview/IndexPage.php index 1183435fa4..3789b62c6d 100644 --- a/src/Sylius/Behat/Page/Admin/ProductReview/IndexPage.php +++ b/src/Sylius/Behat/Page/Admin/ProductReview/IndexPage.php @@ -28,6 +28,11 @@ class IndexPage extends BaseIndexPage implements IndexPageInterface $this->getActionButtonsField($parameters)->pressButton('Reject'); } + public function chooseState(string $state): void + { + $this->getElement('filter_state')->selectOption($state); + } + private function getActionButtonsField(array $parameters): NodeElement { $tableAccessor = $this->getTableAccessor(); @@ -37,4 +42,11 @@ class IndexPage extends BaseIndexPage implements IndexPageInterface return $tableAccessor->getFieldFromRow($table, $row, 'actions'); } + + protected function getDefinedElements(): array + { + return parent::getDefinedElements() + [ + 'filter_state' => '#criteria_status', + ]; + } } diff --git a/src/Sylius/Behat/Page/Admin/ProductReview/IndexPageInterface.php b/src/Sylius/Behat/Page/Admin/ProductReview/IndexPageInterface.php index 1e0f91b8e7..066796f412 100644 --- a/src/Sylius/Behat/Page/Admin/ProductReview/IndexPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/ProductReview/IndexPageInterface.php @@ -20,4 +20,6 @@ interface IndexPageInterface extends BaseIndexPageInterface public function accept(array $parameters): void; public function reject(array $parameters): void; + + public function chooseState(string $state): void; } diff --git a/src/Sylius/Behat/Page/Admin/Promotion/CreatePage.php b/src/Sylius/Behat/Page/Admin/Promotion/CreatePage.php index 89c73f309f..12570a211f 100644 --- a/src/Sylius/Behat/Page/Admin/Promotion/CreatePage.php +++ b/src/Sylius/Behat/Page/Admin/Promotion/CreatePage.php @@ -27,6 +27,13 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface use NamesIt; use SpecifiesItsCode; + public function specifyLabel(string $label, string $localeCode): void + { + $this->getDocument()->find('css', 'div[data-locale="' . $localeCode . '"]')->click(); + + $this->getDocument()->fillField(sprintf('sylius_promotion_translations_%s_label', $localeCode), $label); + } + public function addRule(?string $ruleName): void { $count = count($this->getCollectionItems('rules')); @@ -186,6 +193,18 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface return $this->hasElement('amount'); } + public function hasLabel(string $label, string $localeCode): bool + { + $this->getDocument()->find('css', 'div[data-locale="' . $localeCode . '"]')->click(); + + $labelElement = $this->getDocument()->find('css', sprintf('label:contains("%s")', $label)); + if (null === $labelElement) { + return false; + } + + return $labelElement->hasClass(sprintf('sylius-locale-%s', $localeCode)); + } + protected function getDefinedElements(): array { return [ diff --git a/src/Sylius/Behat/Page/Admin/Promotion/CreatePageInterface.php b/src/Sylius/Behat/Page/Admin/Promotion/CreatePageInterface.php index 74ed83cc5a..30a0fb094a 100644 --- a/src/Sylius/Behat/Page/Admin/Promotion/CreatePageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Promotion/CreatePageInterface.php @@ -20,6 +20,8 @@ interface CreatePageInterface extends BaseCreatePageInterface { public function specifyCode(string $code): void; + public function specifyLabel(string $label, string $localeCode): void; + public function nameIt(string $name): void; public function addRule(?string $ruleName): void; @@ -70,4 +72,6 @@ interface CreatePageInterface extends BaseCreatePageInterface public function checkIfRuleConfigurationFormIsVisible(): bool; public function checkIfActionConfigurationFormIsVisible(): bool; + + public function hasLabel(string $label, string $localeCode): bool; } diff --git a/src/Sylius/Behat/Page/Admin/Promotion/IndexPage.php b/src/Sylius/Behat/Page/Admin/Promotion/IndexPage.php index 132562ac7d..253f27f35a 100644 --- a/src/Sylius/Behat/Page/Admin/Promotion/IndexPage.php +++ b/src/Sylius/Behat/Page/Admin/Promotion/IndexPage.php @@ -50,6 +50,23 @@ class IndexPage extends BaseIndexPage implements IndexPageInterface $this->getDocument()->fillField(sprintf('criteria_%s_value', $field), $value); } + public function chooseArchival(string $isArchival): void + { + $this->getElement('filter_archival')->selectOption($isArchival); + } + + public function isArchivalFilterEnabled(): bool + { + return '1' === $this->getElement('filter_archival')->getValue(); + } + + protected function getDefinedElements(): array + { + return array_merge(parent::getDefinedElements(), [ + 'filter_archival' => '#criteria_archival', + ]); + } + private function getPromotionFieldsWithHeader(PromotionInterface $promotion, string $header): NodeElement { $tableAccessor = $this->getTableAccessor(); diff --git a/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php b/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php index c666e3bcd4..cae26fffd9 100644 --- a/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Promotion/IndexPageInterface.php @@ -23,4 +23,8 @@ interface IndexPageInterface extends BaseIndexPageInterface public function isAbleToManageCouponsFor(PromotionInterface $promotion): bool; public function isCouponBasedFor(PromotionInterface $promotion): bool; + + public function chooseArchival(string $isArchival): void; + + public function isArchivalFilterEnabled(): bool; } diff --git a/src/Sylius/Behat/Page/Admin/Promotion/UpdatePage.php b/src/Sylius/Behat/Page/Admin/Promotion/UpdatePage.php index 817d85a537..f9072e2889 100644 --- a/src/Sylius/Behat/Page/Admin/Promotion/UpdatePage.php +++ b/src/Sylius/Behat/Page/Admin/Promotion/UpdatePage.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Behat\Page\Admin\Promotion; use Behat\Mink\Element\NodeElement; +use Behat\Mink\Exception\ElementNotFoundException; use Sylius\Behat\Behaviour\ChecksCodeImmutability; use Sylius\Behat\Behaviour\CountsChannelBasedErrors; use Sylius\Behat\Behaviour\NamesIt; @@ -166,6 +167,21 @@ class UpdatePage extends BaseUpdatePage implements UpdatePageInterface return $this->countChannelErrors($this->getElement('rules'), $channelCode); } + /** + * @throws ElementNotFoundException + */ + public function getValidationMessageForTranslation(string $element, string $localeCode): string + { + $foundElement = $this->getElement($element, ['%localeCode%' => $localeCode])->getParent(); + + $validationMessage = $foundElement->find('css', '.sylius-validation-error'); + if (null === $validationMessage) { + throw new ElementNotFoundException($this->getSession(), 'Validation message', 'css', '.sylius-validation-error'); + } + + return $validationMessage->getText(); + } + protected function getCodeElement(): NodeElement { return $this->getElement('code'); @@ -183,6 +199,7 @@ class UpdatePage extends BaseUpdatePage implements UpdatePageInterface 'ends_at_date' => '#sylius_promotion_endsAt_date', 'ends_at_time' => '#sylius_promotion_endsAt_time', 'exclusive' => '#sylius_promotion_exclusive', + 'label' => '#sylius_promotion_translations_%localeCode%_label', 'name' => '#sylius_promotion_name', 'order_percentage_action_field' => '[id^="sylius_promotion_actions_"][id$="_configuration_percentage"]', 'priority' => '#sylius_promotion_priority', diff --git a/src/Sylius/Behat/Page/Admin/Promotion/UpdatePageInterface.php b/src/Sylius/Behat/Page/Admin/Promotion/UpdatePageInterface.php index e1fb42f0d8..3f3804b1ff 100644 --- a/src/Sylius/Behat/Page/Admin/Promotion/UpdatePageInterface.php +++ b/src/Sylius/Behat/Page/Admin/Promotion/UpdatePageInterface.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace Sylius\Behat\Page\Admin\Promotion; +use Behat\Mink\Exception\ElementNotFoundException; use Sylius\Behat\Page\Admin\Crud\UpdatePageInterface as BaseUpdatePageInterface; interface UpdatePageInterface extends BaseUpdatePageInterface @@ -64,4 +65,9 @@ interface UpdatePageInterface extends BaseUpdatePageInterface public function getActionValidationErrorsCount(string $channelCode): int; public function getRuleValidationErrorsCount(string $channelCode): int; + + /** + * @throws ElementNotFoundException + */ + public function getValidationMessageForTranslation(string $element, string $localeCode): string; } diff --git a/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePage.php b/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePage.php index 73ebd9ae60..3cc5545540 100644 --- a/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePage.php +++ b/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePage.php @@ -16,6 +16,7 @@ namespace Sylius\Behat\Page\Admin\PromotionCoupon; use Behat\Mink\Element\NodeElement; use Sylius\Behat\Behaviour\ChecksCodeImmutability; use Sylius\Behat\Page\Admin\Crud\UpdatePage as BaseUpdatePage; +use Webmozart\Assert\Assert; class UpdatePage extends BaseUpdatePage implements UpdatePageInterface { @@ -38,6 +39,20 @@ class UpdatePage extends BaseUpdatePage implements UpdatePageInterface $this->getDocument()->fillField('Usage limit', $limit); } + public function isReusableFromCancelledOrders(): bool + { + return $this->getElement('reusable_from_cancelled_orders')->isChecked(); + } + + public function toggleReusableFromCancelledOrders(bool $reusable): void + { + $toggle = $this->getElement('reusable_from_cancelled_orders'); + + Assert::notSame($toggle->isChecked(), $reusable); + + $reusable ? $toggle->check() : $toggle->uncheck(); + } + protected function getCodeElement(): NodeElement { return $this->getElement('code'); @@ -50,6 +65,7 @@ class UpdatePage extends BaseUpdatePage implements UpdatePageInterface 'expires_at' => '#sylius_promotion_coupon_expiresAt', 'usage_limit' => '#sylius_promotion_coupon_usageLimit', 'per_customer_usage_limit' => '#sylius_promotion_coupon_perCustomerUsageLimit', + 'reusable_from_cancelled_orders' => '#sylius_promotion_coupon_reusableFromCancelledOrders', ]); } } diff --git a/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePageInterface.php b/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePageInterface.php index 9cfc8f1113..9376fee036 100644 --- a/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePageInterface.php +++ b/src/Sylius/Behat/Page/Admin/PromotionCoupon/UpdatePageInterface.php @@ -24,4 +24,8 @@ interface UpdatePageInterface extends BaseUpdatePageInterface public function setExpiresAt(\DateTimeInterface $date): void; public function setUsageLimit(string $limit): void; + + public function isReusableFromCancelledOrders(): bool; + + public function toggleReusableFromCancelledOrders(bool $reusable): void; } diff --git a/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePage.php b/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePage.php index 7074de8681..595b4f55b9 100644 --- a/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePage.php +++ b/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePage.php @@ -75,21 +75,15 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface public function getValidationMessageForAmount(string $channelCode): string { $foundElement = $this->getFieldElement('amount', ['%channelCode%' => $channelCode]); - if (null === $foundElement) { - throw new ElementNotFoundException($this->getSession(), 'Field element'); - } - $validationMessage = $foundElement->find('css', '.sylius-validation-error'); - if (null === $validationMessage) { - throw new ElementNotFoundException( - $this->getSession(), - 'Validation message', - 'css', - '.sylius-validation-error', - ); - } + return $this->getValidationMessageForElement($foundElement); + } - return $validationMessage->getText(); + public function getValidationMessageForRuleAmount(string $channelCode): string + { + $foundElement = $this->getChannelConfigurationOfLastRule($channelCode); + + return $this->getValidationMessageForElement($foundElement); } public function addRule(string $ruleName): void @@ -126,6 +120,7 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface 'channel' => '#sylius_shipping_method_channels .ui.checkbox:contains("%channel%")', 'calculator' => '#sylius_shipping_method_calculator', 'calculator_configuration' => '.ui.segment.configuration', + 'weight' => '[id*="sylius_shipping_method_rules_"][id*="_configuration_weight"]', 'code' => '#sylius_shipping_method_code', 'name' => '#sylius_shipping_method_translations_en_US_name', 'zone' => '#sylius_shipping_method_zone', @@ -133,6 +128,25 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface ]); } + private function getValidationMessageForElement(NodeElement $element): string + { + if (null === $element) { + throw new ElementNotFoundException($this->getSession(), 'Field element'); + } + + $validationMessage = $element->find('css', '.sylius-validation-error'); + if (null === $validationMessage) { + throw new ElementNotFoundException( + $this->getSession(), + 'Validation message', + 'css', + '.sylius-validation-error', + ); + } + + return $validationMessage->getText(); + } + /** * @throws ElementNotFoundException */ @@ -167,7 +181,7 @@ class CreatePage extends BaseCreatePage implements CreatePageInterface return $items; } - private function getChannelConfigurationOfLastRule(string $channelCode): NodeElement + private function getChannelConfigurationOfLastRule(string $channelCode): ?NodeElement { return $this ->getLastCollectionItem('rules') diff --git a/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePageInterface.php b/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePageInterface.php index 4612a1d529..aee0845836 100644 --- a/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePageInterface.php +++ b/src/Sylius/Behat/Page/Admin/ShippingMethod/CreatePageInterface.php @@ -32,13 +32,14 @@ interface CreatePageInterface extends BaseCreatePageInterface public function chooseCalculator(string $name): void; - public function checkChannel($channelName): void; + public function checkChannel(string $channelName): void; - /** - * @throws ElementNotFoundException - */ + /** @throws ElementNotFoundException */ public function getValidationMessageForAmount(string $channelCode): string; + /** @throws ElementNotFoundException */ + public function getValidationMessageForRuleAmount(string $channelCode): string; + public function addRule(string $ruleName): void; public function selectRuleOption(string $option, string $value, bool $multiple = false): void; diff --git a/src/Sylius/Behat/Page/Admin/TaxRate/UpdatePage.php b/src/Sylius/Behat/Page/Admin/TaxRate/UpdatePage.php index c235424b71..dbe3860bb6 100644 --- a/src/Sylius/Behat/Page/Admin/TaxRate/UpdatePage.php +++ b/src/Sylius/Behat/Page/Admin/TaxRate/UpdatePage.php @@ -26,6 +26,11 @@ class UpdatePage extends BaseUpdatePage implements UpdatePageInterface $this->getDocument()->selectFieldOption('Zone', 'Select'); } + public function isIncludedInPrice(): bool + { + return (bool) $this->getElement('included_in_price')->getValue(); + } + protected function getCodeElement(): NodeElement { return $this->getElement('code'); @@ -40,6 +45,7 @@ class UpdatePage extends BaseUpdatePage implements UpdatePageInterface 'code' => '#sylius_tax_rate_code', 'name' => '#sylius_tax_rate_name', 'zone' => '#sylius_tax_rate_zone', + 'included_in_price' => '#sylius_tax_rate_includedInPrice', ]); } } diff --git a/src/Sylius/Behat/Page/Shop/Account/Order/IndexPage.php b/src/Sylius/Behat/Page/Shop/Account/Order/IndexPage.php index 20ecc6ddc5..799427afac 100644 --- a/src/Sylius/Behat/Page/Shop/Account/Order/IndexPage.php +++ b/src/Sylius/Behat/Page/Shop/Account/Order/IndexPage.php @@ -51,6 +51,11 @@ class IndexPage extends SymfonyPage implements IndexPageInterface $link->click(); } + public function hasFlashMessage(string $message): bool + { + return str_contains($this->getElement('flash_message')->getText(), $message); + } + public function isOrderWithNumberInTheList($number): bool { try { @@ -74,6 +79,7 @@ class IndexPage extends SymfonyPage implements IndexPageInterface { return array_merge(parent::getDefinedElements(), [ 'customer_orders' => '[data-test-grid-table]', + 'flash_message' => '[data-test-flash-message]', 'last_order' => '[data-test-grid-table-body] [data-test-row]:last-child [data-test-actions]', ]); } diff --git a/src/Sylius/Behat/Page/Shop/Account/Order/IndexPageInterface.php b/src/Sylius/Behat/Page/Shop/Account/Order/IndexPageInterface.php index 92809778d1..93a3dd563b 100644 --- a/src/Sylius/Behat/Page/Shop/Account/Order/IndexPageInterface.php +++ b/src/Sylius/Behat/Page/Shop/Account/Order/IndexPageInterface.php @@ -22,6 +22,8 @@ interface IndexPageInterface extends SymfonyPageInterface public function changePaymentMethod(OrderInterface $order); + public function hasFlashMessage(string $message): bool; + public function isOrderWithNumberInTheList(string $number): bool; public function openLastOrderPage(): void; diff --git a/src/Sylius/Behat/Page/Shop/Account/RegisterThankYouPage.php b/src/Sylius/Behat/Page/Shop/Account/RegisterThankYouPage.php new file mode 100644 index 0000000000..a4150ce72d --- /dev/null +++ b/src/Sylius/Behat/Page/Shop/Account/RegisterThankYouPage.php @@ -0,0 +1,24 @@ +hasItemWith($code, '[data-test-product-variant-code]'); } + public function hasItemWithInsufficientStock(string $productName): bool + { + $product = $this->getElement('product_row', ['%name%' => $productName]); + + return str_contains($product->getText(), 'Insufficient stock'); + } + public function hasProductOutOfStockValidationMessage(ProductInterface $product): bool { $message = sprintf('%s does not have sufficient stock.', $product->getName()); diff --git a/src/Sylius/Behat/Page/Shop/Cart/SummaryPageInterface.php b/src/Sylius/Behat/Page/Shop/Cart/SummaryPageInterface.php index 838ff315fb..1c8a3ca96e 100644 --- a/src/Sylius/Behat/Page/Shop/Cart/SummaryPageInterface.php +++ b/src/Sylius/Behat/Page/Shop/Cart/SummaryPageInterface.php @@ -66,6 +66,8 @@ interface SummaryPageInterface extends PageInterface public function hasItemWithOptionValue(string $productName, string $optionName, string $optionValue): bool; + public function hasItemWithInsufficientStock(string $productName): bool; + public function hasProductOutOfStockValidationMessage(ProductInterface $product): bool; public function isEmpty(): bool; diff --git a/src/Sylius/Behat/Resources/config/services.xml b/src/Sylius/Behat/Resources/config/services.xml index eb3ee75240..7068be76df 100644 --- a/src/Sylius/Behat/Resources/config/services.xml +++ b/src/Sylius/Behat/Resources/config/services.xml @@ -13,6 +13,7 @@ + @@ -60,6 +61,10 @@ + + + + @@ -94,11 +99,15 @@ - 100000 + 1000000 7 + + + + @@ -107,18 +116,20 @@ - - - - + + + - - + + + + + diff --git a/src/Sylius/Behat/Resources/config/services/_calendar_services.php b/src/Sylius/Behat/Resources/config/services/_calendar_services.php new file mode 100644 index 0000000000..469be5dcfa --- /dev/null +++ b/src/Sylius/Behat/Resources/config/services/_calendar_services.php @@ -0,0 +1,29 @@ +import($baseSyliusCalendarDir); + + return; + } + + $appSyliusCalendarDir = __DIR__ . '/../../../../../../../../sylius/calendar/tests/Behat/Resources/services.yaml'; + if (file_exists($appSyliusCalendarDir)) { + $containerConfigurator->import($appSyliusCalendarDir); + } +}; diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml index ff571d983f..dfd81b9692 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/admin.xml @@ -23,6 +23,12 @@ + + + + + + @@ -34,6 +40,16 @@ + + + + + + + + + + @@ -43,10 +59,24 @@ + + + + + + + + + + + + + + @@ -73,12 +103,35 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -89,14 +142,21 @@ + %sylius.security.new_api_route% + + + + + + @@ -105,9 +165,17 @@ - - + + + + + + + + + + @@ -119,6 +187,7 @@ + @@ -136,6 +205,7 @@ + %sylius.security.new_api_route% @@ -144,6 +214,7 @@ + %sylius.security.new_api_route% @@ -154,6 +225,15 @@ + + + + + + + + + @@ -169,12 +249,28 @@ + + + + + + + + + + + + + + + + @@ -187,5 +283,45 @@ %sylius.security.new_api_route% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/common.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/common.xml new file mode 100644 index 0000000000..df7bab3c03 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/common.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Behat/Resources/config/services/contexts/api/shop.xml b/src/Sylius/Behat/Resources/config/services/contexts/api/shop.xml index 732a9643d4..c3fe505dd6 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/api/shop.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/api/shop.xml @@ -43,7 +43,7 @@ - + %sylius.security.new_api_route% @@ -77,7 +77,7 @@ - + @@ -88,6 +88,8 @@ + + %sylius.security.new_api_route% @@ -122,6 +124,7 @@ + @@ -180,5 +183,19 @@ + + + + + + + + + + + + + %sylius.security.new_api_route% + diff --git a/src/Sylius/Behat/Resources/config/services/contexts/cli.xml b/src/Sylius/Behat/Resources/config/services/contexts/cli.xml index 74f7d2e476..f31eaf8c27 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/cli.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/cli.xml @@ -27,5 +27,11 @@ + + + + + + diff --git a/src/Sylius/Behat/Resources/config/services/contexts/domain.xml b/src/Sylius/Behat/Resources/config/services/contexts/domain.xml index 3e3a9b38e6..f963892fab 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/domain.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/domain.xml @@ -25,7 +25,7 @@ - + @@ -33,6 +33,12 @@ + + + + + + @@ -43,6 +49,7 @@ + diff --git a/src/Sylius/Behat/Resources/config/services/contexts/hook.xml b/src/Sylius/Behat/Resources/config/services/contexts/hook.xml index 5c2413a95a..200e488e51 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/hook.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/hook.xml @@ -31,5 +31,9 @@ + + + + diff --git a/src/Sylius/Behat/Resources/config/services/contexts/setup.xml b/src/Sylius/Behat/Resources/config/services/contexts/setup.xml index 3badf9bf09..fddfd5459a 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/setup.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/setup.xml @@ -31,8 +31,9 @@ + - + @@ -53,7 +54,6 @@ - @@ -110,15 +110,17 @@ - + - + + + @@ -135,6 +137,12 @@ + + + + + + @@ -149,10 +157,13 @@ - + + + + @@ -185,7 +196,7 @@ - + @@ -198,10 +209,10 @@ - + @@ -280,6 +291,7 @@ + %sylius.shop_user.token.password_reset.ttl% @@ -296,7 +308,7 @@ - + diff --git a/src/Sylius/Behat/Resources/config/services/contexts/transform.xml b/src/Sylius/Behat/Resources/config/services/contexts/transform.xml index 790a0ad1dd..1fb1c7d193 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/transform.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/transform.xml @@ -91,6 +91,10 @@ + + + + @@ -175,5 +179,9 @@ + + + + diff --git a/src/Sylius/Behat/Resources/config/services/contexts/ui.xml b/src/Sylius/Behat/Resources/config/services/contexts/ui.xml index db07f360bc..821056a791 100644 --- a/src/Sylius/Behat/Resources/config/services/contexts/ui.xml +++ b/src/Sylius/Behat/Resources/config/services/contexts/ui.xml @@ -68,6 +68,9 @@ + + + @@ -117,6 +120,8 @@ + + @@ -202,7 +207,6 @@ - @@ -238,7 +242,7 @@ - + @@ -249,7 +253,9 @@ + + @@ -292,6 +298,7 @@ + @@ -404,6 +411,7 @@ + @@ -412,7 +420,10 @@ + + + @@ -518,6 +529,7 @@ + @@ -539,11 +551,12 @@ + - + @@ -567,5 +580,9 @@ + + + + diff --git a/src/Sylius/Behat/Resources/config/services/elements/admin.xml b/src/Sylius/Behat/Resources/config/services/elements/admin.xml index 2b636ee383..839891a9a6 100644 --- a/src/Sylius/Behat/Resources/config/services/elements/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/elements/admin.xml @@ -29,5 +29,29 @@ + + + + + + + + diff --git a/src/Sylius/Behat/Resources/config/services/elements/product.xml b/src/Sylius/Behat/Resources/config/services/elements/product.xml index ff3ed26e30..72adc85369 100644 --- a/src/Sylius/Behat/Resources/config/services/elements/product.xml +++ b/src/Sylius/Behat/Resources/config/services/elements/product.xml @@ -28,6 +28,7 @@ + diff --git a/src/Sylius/Behat/Resources/config/services/pages/admin.xml b/src/Sylius/Behat/Resources/config/services/pages/admin.xml index 212f49c8f2..6d3afd3ee2 100644 --- a/src/Sylius/Behat/Resources/config/services/pages/admin.xml +++ b/src/Sylius/Behat/Resources/config/services/pages/admin.xml @@ -13,7 +13,6 @@ - @@ -22,13 +21,15 @@ + - + + diff --git a/src/Sylius/Behat/Resources/config/services/pages/admin/price_history.xml b/src/Sylius/Behat/Resources/config/services/pages/admin/price_history.xml new file mode 100644 index 0000000000..3a8e1999a3 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/services/pages/admin/price_history.xml @@ -0,0 +1,20 @@ + + + + + + + + sylius_admin_channel_pricing_log_entry_index + + + diff --git a/src/Sylius/Behat/Resources/config/services/pages/shop/account.xml b/src/Sylius/Behat/Resources/config/services/pages/shop/account.xml index 8277de2b9d..237df40853 100644 --- a/src/Sylius/Behat/Resources/config/services/pages/shop/account.xml +++ b/src/Sylius/Behat/Resources/config/services/pages/shop/account.xml @@ -23,6 +23,7 @@ Sylius\Behat\Page\Shop\Account\Order\ShowPage Sylius\Behat\Page\Shop\Account\ProfileUpdatePage Sylius\Behat\Page\Shop\Account\RegisterPage + Sylius\Behat\Page\Shop\Account\RegisterThankYouPage Sylius\Behat\Page\Shop\Account\RequestPasswordResetPage Sylius\Behat\Page\Shop\Account\ResetPasswordPage Sylius\Behat\Page\Shop\Account\VerificationPage @@ -46,6 +47,7 @@ + diff --git a/src/Sylius/Behat/Resources/config/suites.yml b/src/Sylius/Behat/Resources/config/suites.yml index 21fe8baeb4..9845d1d4d4 100644 --- a/src/Sylius/Behat/Resources/config/suites.yml +++ b/src/Sylius/Behat/Resources/config/suites.yml @@ -9,6 +9,7 @@ imports: - suites/api/account/login.yml - suites/api/addressing/managing_countries.yml - suites/api/addressing/managing_zones.yml + - suites/api/admin/dashboard.yaml - suites/api/admin/login.yml - suites/api/admin/security.yml - suites/api/cart/accessing_cart.yml @@ -25,24 +26,33 @@ imports: - suites/api/homepage/viewing_products.yml - suites/api/inventory/cart_inventory.yml - suites/api/inventory/checkout_inventory.yaml + - suites/api/inventory/managing_inventory.yaml - suites/api/locale/locales.yaml - suites/api/locale/managing_locales.yml - suites/api/order/managing_orders.yml + - suites/api/order/modifying_placed_order_address.yaml + - suites/api/order/order_history.yaml + - suites/api/payment/managing_payment_methods.yaml - suites/api/payment/managing_payments.yml - suites/api/product/adding_product_review.yml - suites/api/product/managing_product_association_types.yml + - suites/api/product/managing_product_attributes.yml - suites/api/product/managing_product_options.yml - suites/api/product/managing_product_reviews.yml - suites/api/product/managing_product_variants.yml - suites/api/product/managing_product_variants_ajax.yml - suites/api/product/managing_products.yml - - suites/api/product/viewing_products.yml + - suites/api/product/viewing_price_history.yml + - suites/api/product/viewing_price_history_after_catalog_promotions.yml + - suites/api/product/viewing_product_in_admin_panel.yaml - suites/api/product/viewing_product_reviews.yml - suites/api/product/viewing_product_variants.yml + - suites/api/product/viewing_products.yml - suites/api/promotion/applying_catalog_promotions.yml - suites/api/promotion/applying_promotion_coupon.yml - suites/api/promotion/applying_promotion_rules.yml - suites/api/promotion/managing_catalog_promotions.yml + - suites/api/promotion/managing_promotion_coupons.yml - suites/api/promotion/managing_promotions.yml - suites/api/promotion/receiving_discount.yml - suites/api/promotion/removing_catalog_promotions.yml @@ -53,11 +63,16 @@ imports: - suites/api/shipping/managing_shipping_methods.yml - suites/api/taxation/applying_taxes.yml - suites/api/taxation/managing_tax_categories.yml + - suites/api/taxation/managing_tax_rates.yml - suites/api/taxon/managing_taxons.yml + - suites/api/taxon/managing_taxons_ajax.yml - suites/api/user/managing_administrators.yml - suites/api/user/managing_customer_groups.yml + - suites/api/user/managing_customers.yml + - suites/api/user/managing_users.yml - suites/cli/canceling_unpaid_orders.yml + - suites/cli/change_admin_password.yml - suites/cli/installer.yml - suites/cli/showing_available_plugins.yml @@ -65,6 +80,7 @@ imports: - suites/domain/cart/shopping_cart.yml - suites/domain/order/managing_orders.yml + - suites/domain/product/managing_price_history.yml - suites/domain/product/managing_product_variants.yml - suites/domain/product/managing_products.yml - suites/domain/promotion/managing_promotion_coupons.yml @@ -78,7 +94,7 @@ imports: - suites/ui/account/login.yml - suites/ui/addressing/managing_countries.yml - suites/ui/addressing/managing_zones.yml - - suites/ui/admin/dashboard.yml + - suites/ui/admin/dashboard.yaml - suites/ui/admin/impersonating_customers.yml - suites/ui/admin/locale.yml - suites/ui/admin/login.yml @@ -103,11 +119,12 @@ imports: - suites/ui/locale/locales.yml - suites/ui/locale/managing_locales.yml - suites/ui/order/managing_orders.yml - - suites/ui/order/modifying_address.yml + - suites/ui/order/modifying_placed_order_address.yaml - suites/ui/order/order_history.yml - suites/ui/payment/managing_payment_methods.yml - suites/ui/payment/managing_payments.yml - suites/ui/product/accessing_edit_page_from_product_show_page.yml + - suites/ui/product/accessing_price_history.yml - suites/ui/product/adding_product_review.yml - suites/ui/product/managing_product_association_types.yml - suites/ui/product/managing_product_attributes.yml @@ -115,8 +132,11 @@ imports: - suites/ui/product/managing_product_reviews.yml - suites/ui/product/managing_product_variants.yml - suites/ui/product/managing_products.yml + - suites/ui/product/viewing_product_in_admin_panel.yaml - suites/ui/product/viewing_product_reviews.yml - suites/ui/product/viewing_products.yml + - suites/ui/product/viewing_price_history.yaml + - suites/ui/product/viewing_price_history_after_catalog_promotions.yaml - suites/ui/promotion/applying_catalog_promotions.yml - suites/ui/promotion/applying_promotion_coupon.yml - suites/ui/promotion/applying_promotion_rules.yml diff --git a/src/Sylius/Behat/Resources/config/suites/api/account/address_book.yml b/src/Sylius/Behat/Resources/config/suites/api/account/address_book.yml index 7a8571e8c5..07485bfbeb 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/account/address_book.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/account/address_book.yml @@ -32,6 +32,7 @@ default: - sylius.behat.context.api.shop.address - sylius.behat.context.api.shop.checkout + - sylius.behat.context.api.shop.save filters: tags: "@address_book&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/account/customer.yml b/src/Sylius/Behat/Resources/config/suites/api/account/customer.yml index 5b6d43af22..cc93f728e5 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/account/customer.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/account/customer.yml @@ -33,12 +33,15 @@ default: - sylius.behat.context.setup.user - sylius.behat.context.setup.zone + - sylius.behat.context.api.email + - sylius.behat.context.api.shop.checkout - sylius.behat.context.api.shop.customer + - sylius.behat.context.api.shop.login - sylius.behat.context.api.shop.order - sylius.behat.context.api.shop.order_item - - sylius.behat.context.api.shop.checkout - - sylius.behat.context.api.shop.login - sylius.behat.context.api.shop.payment + - sylius.behat.context.api.shop.response + - sylius.behat.context.api.shop.save - sylius.behat.context.api.shop.shipment filters: diff --git a/src/Sylius/Behat/Resources/config/suites/api/addressing/managing_countries.yml b/src/Sylius/Behat/Resources/config/suites/api/addressing/managing_countries.yml index 2f226bd07c..0b4ac9fbf9 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/addressing/managing_countries.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/addressing/managing_countries.yml @@ -19,6 +19,8 @@ default: - sylius.behat.context.setup.zone - sylius.behat.context.api.admin.managing_countries + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_countries&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/addressing/managing_zones.yml b/src/Sylius/Behat/Resources/config/suites/api/addressing/managing_zones.yml index a032a73ff0..e92b31b52b 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/addressing/managing_zones.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/addressing/managing_zones.yml @@ -19,6 +19,8 @@ default: - sylius.behat.context.setup.zone - sylius.behat.context.api.admin.managing_zones + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_zones&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/admin/dashboard.yaml b/src/Sylius/Behat/Resources/config/suites/api/admin/dashboard.yaml new file mode 100644 index 0000000000..ed29a2372a --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/admin/dashboard.yaml @@ -0,0 +1,39 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_dashboard: + contexts: + - sylius.behat.context.hook.doctrine_orm + - Sylius\Calendar\Tests\Behat\Context\Hook\CalendarContext + + - sylius.behat.context.transform.admin + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.user + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.currency + - sylius.behat.context.setup.geographical + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.promotion + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.zone + + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.order + - sylius.behat.context.transform.product + + - sylius.behat.context.transform.shared_storage + + - Sylius\Behat\Context\Api\Admin\DashboardContext + - sylius.behat.context.api.admin.login + + filters: + tags: "@admin_dashboard&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/channel/managing_channels.yml b/src/Sylius/Behat/Resources/config/suites/api/channel/managing_channels.yml index beee73332f..1f68d15ceb 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/channel/managing_channels.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/channel/managing_channels.yml @@ -27,6 +27,10 @@ default: - sylius.behat.context.setup.zone - sylius.behat.context.api.admin.managing_channels + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + - Sylius\Behat\Context\Api\Admin\ManagingChannelsBillingDataContext + - Sylius\Behat\Context\Api\Admin\ManagingChannelPriceHistoryConfigsContext filters: tags: "@managing_channels&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/checkout/checkout.yml b/src/Sylius/Behat/Resources/config/suites/api/checkout/checkout.yml index 7d0ed5b26e..8114210110 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/checkout/checkout.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/checkout/checkout.yml @@ -18,6 +18,7 @@ default: - sylius.behat.context.transform.product - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.promotion - sylius.behat.context.transform.province - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_category diff --git a/src/Sylius/Behat/Resources/config/suites/api/checkout/paying_for_order.yml b/src/Sylius/Behat/Resources/config/suites/api/checkout/paying_for_order.yml index 9ec0c6f54e..7918de3352 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/checkout/paying_for_order.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/checkout/paying_for_order.yml @@ -12,10 +12,15 @@ default: - sylius.behat.context.transform.channel - sylius.behat.context.transform.country - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.locale - sylius.behat.context.transform.payment - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.tax_category + - sylius.behat.context.transform.tax_rate + - sylius.behat.context.transform.user - sylius.behat.context.transform.zone - sylius.behat.context.setup.cart @@ -23,15 +28,19 @@ default: - sylius.behat.context.setup.checkout - sylius.behat.context.setup.geographical - sylius.behat.context.setup.locale + - sylius.behat.context.setup.order - sylius.behat.context.setup.payment - sylius.behat.context.setup.product - sylius.behat.context.setup.shipping - sylius.behat.context.setup.shop_api_security + - sylius.behat.context.setup.taxation - sylius.behat.context.setup.zone - sylius.behat.context.api.shop.cart - sylius.behat.context.api.shop.checkout + - sylius.behat.context.api.shop.checkout.complete - sylius.behat.context.api.shop.order + - sylius.behat.context.api.shop.response filters: tags: "@paying_for_order&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/currency/managing_exchange_rates.yml b/src/Sylius/Behat/Resources/config/suites/api/currency/managing_exchange_rates.yml index 9e1e4ea982..30ba4534d6 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/currency/managing_exchange_rates.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/currency/managing_exchange_rates.yml @@ -17,6 +17,8 @@ default: - sylius.behat.context.setup.exchange_rate - sylius.behat.context.api.admin.managing_exchange_rates + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_exchange_rates&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/inventory/checkout_inventory.yaml b/src/Sylius/Behat/Resources/config/suites/api/inventory/checkout_inventory.yaml index 91e4f9a625..98c3f94a45 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/inventory/checkout_inventory.yaml +++ b/src/Sylius/Behat/Resources/config/suites/api/inventory/checkout_inventory.yaml @@ -6,12 +6,15 @@ default: api_checkout_inventory: contexts: - sylius.behat.context.hook.doctrine_orm - + + - sylius.behat.context.setup.admin_api_security - sylius.behat.context.setup.channel + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment - sylius.behat.context.setup.product - sylius.behat.context.setup.shipping - - sylius.behat.context.setup.payment - sylius.behat.context.setup.shop_api_security + - sylius.behat.context.setup.user - sylius.behat.context.transform.address - sylius.behat.context.transform.cart @@ -22,9 +25,10 @@ default: - sylius.behat.context.transform.product - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_method - + - sylius.behat.context.api.shop.cart - sylius.behat.context.api.shop.checkout + - sylius.behat.context.api.admin.managing_product_variants filters: tags: "@checkout_inventory&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/inventory/managing_inventory.yaml b/src/Sylius/Behat/Resources/config/suites/api/inventory/managing_inventory.yaml new file mode 100644 index 0000000000..32b0066ab9 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/inventory/managing_inventory.yaml @@ -0,0 +1,35 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_managing_inventory: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.shop_api_security + - sylius.behat.context.setup.user + + - sylius.behat.context.transform.address + - sylius.behat.context.transform.cart + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.order + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_method + + - sylius.behat.context.api.admin.managing_product_variants + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + + filters: + tags: "@managing_inventory&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/locale/managing_locales.yml b/src/Sylius/Behat/Resources/config/suites/api/locale/managing_locales.yml index 5ceb853f83..e53fe27e5b 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/locale/managing_locales.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/locale/managing_locales.yml @@ -14,6 +14,7 @@ default: - sylius.behat.context.setup.admin_api_security - sylius.behat.context.setup.channel - sylius.behat.context.setup.locale + - sylius.behat.context.setup.product - sylius.behat.context.api.admin.managing_locales filters: diff --git a/src/Sylius/Behat/Resources/config/suites/api/order/managing_orders.yml b/src/Sylius/Behat/Resources/config/suites/api/order/managing_orders.yml index 5169554556..b134f8d00a 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/order/managing_orders.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/order/managing_orders.yml @@ -10,15 +10,28 @@ default: - Sylius\Calendar\Tests\Behat\Context\Hook\CalendarContext - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.admin_user + - sylius.behat.context.setup.cart - sylius.behat.context.setup.channel + - sylius.behat.context.setup.currency + - sylius.behat.context.setup.customer + - sylius.behat.context.setup.geographical + - sylius.behat.context.setup.locale - sylius.behat.context.setup.order - sylius.behat.context.setup.payment - sylius.behat.context.setup.product + - sylius.behat.context.setup.product_taxon - sylius.behat.context.setup.promotion - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.shop_api_security - sylius.behat.context.setup.taxation + - sylius.behat.context.setup.taxonomy + - sylius.behat.context.setup.zone + + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext - sylius.behat.context.transform.address + - sylius.behat.context.transform.cart - sylius.behat.context.transform.channel - sylius.behat.context.transform.country - sylius.behat.context.transform.currency @@ -39,6 +52,8 @@ default: - sylius.behat.context.api.admin.managing_orders - sylius.behat.context.api.email + + - sylius.behat.context.api.shop.checkout filters: tags: "@managing_orders&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/order/modifying_placed_order_address.yaml b/src/Sylius/Behat/Resources/config/suites/api/order/modifying_placed_order_address.yaml new file mode 100644 index 0000000000..2f1f158a6d --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/order/modifying_placed_order_address.yaml @@ -0,0 +1,44 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_modifying_placed_order_address: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.currency + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.product_taxon + - sylius.behat.context.setup.promotion + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.taxation + - sylius.behat.context.setup.taxonomy + + - sylius.behat.context.transform.address + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.coupon + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.order + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.tax_category + - sylius.behat.context.transform.tax_rate + - sylius.behat.context.transform.taxon + - sylius.behat.context.transform.zone + + - sylius.behat.context.transform.shared_storage + + - sylius.behat.context.api.admin.managing_orders + - sylius.behat.context.api.admin.managing_placed_order_addresses + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + + filters: + tags: "@modifying_placed_order_address&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/order/order_history.yaml b/src/Sylius/Behat/Resources/config/suites/api/order/order_history.yaml new file mode 100644 index 0000000000..556369a06e --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/order/order_history.yaml @@ -0,0 +1,37 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_order_history: + contexts: + - sylius.behat.context.hook.doctrine_orm + - sylius.behat.context.hook.session + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.shipping + + - sylius.behat.context.transform.address + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.country + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.order + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.zone + + - sylius.behat.context.transform.shared_storage + + - sylius.behat.context.api.admin.managing_orders + - sylius.behat.context.api.admin.managing_placed_order_addresses + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + + filters: + tags: "@order_history&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/payment/managing_payment_methods.yaml b/src/Sylius/Behat/Resources/config/suites/api/payment/managing_payment_methods.yaml new file mode 100644 index 0000000000..809b63b81d --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/payment/managing_payment_methods.yaml @@ -0,0 +1,35 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_managing_payment_methods: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.currency + - sylius.behat.context.setup.locale + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.user + - sylius.behat.context.setup.zone + + - sylius.behat.context.transform.address + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.locale + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.shared_storage + + - sylius.behat.context.api.admin.save + - sylius.behat.context.api.admin.response + - Sylius\Behat\Context\Api\Admin\ManagingPaymentMethodsContext + + filters: + tags: "@managing_payment_methods&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_association_types.yml b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_association_types.yml index 74fe836880..3a987938ab 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_association_types.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_association_types.yml @@ -19,6 +19,8 @@ default: - sylius.behat.context.setup.product_association - sylius.behat.context.api.admin.managing_product_association_types + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_product_association_types&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_attributes.yml b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_attributes.yml new file mode 100644 index 0000000000..e566ad719f --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_attributes.yml @@ -0,0 +1,25 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_managing_product_attributes: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.locale + - sylius.behat.context.transform.shared_storage + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.locale + - sylius.behat.context.setup.product + - sylius.behat.context.setup.product_attribute + + - sylius.behat.context.api.admin.managing_product_attributes + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + + filters: + tags: "@managing_product_attributes&&@api" + javascript: false diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_options.yml b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_options.yml index d88773a38b..d1c2e894ac 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_options.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_options.yml @@ -17,6 +17,8 @@ default: - sylius.behat.context.setup.admin_api_security - sylius.behat.context.api.admin.managing_product_options + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_product_options&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_reviews.yml b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_reviews.yml index 3c2e1cec2c..c2925aedca 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_reviews.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_reviews.yml @@ -20,6 +20,8 @@ default: - sylius.behat.context.setup.product_review - sylius.behat.context.api.admin.managing_product_reviews + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_product_reviews&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml index d4a62b9b7e..1aa96fe213 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/managing_product_variants.yml @@ -12,8 +12,11 @@ default: - sylius.behat.context.transform.locale - sylius.behat.context.transform.lexical - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_option + - sylius.behat.context.transform.product_option_value - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_category - sylius.behat.context.transform.channel - sylius.behat.context.transform.customer - sylius.behat.context.transform.payment @@ -32,8 +35,14 @@ default: - sylius.behat.context.setup.product - sylius.behat.context.setup.admin_api_security - sylius.behat.context.setup.taxonomy + - Sylius\Behat\Context\Setup\CatalogPromotionContext - sylius.behat.context.api.admin.managing_product_variants + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + - Sylius\Behat\Context\Api\Admin\BrowsingProductVariantsContext + - Sylius\Behat\Context\Api\Admin\ChannelPricingLogEntryContext + filters: tags: "@managing_product_variants&&@api" javascript: false diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/managing_products.yml b/src/Sylius/Behat/Resources/config/suites/api/product/managing_products.yml index 1227adeed3..17d6d0b772 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/managing_products.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/managing_products.yml @@ -17,7 +17,9 @@ default: - sylius.behat.context.transform.payment - sylius.behat.context.transform.product - sylius.behat.context.transform.product_association_type + - sylius.behat.context.transform.product_attribute - sylius.behat.context.transform.product_option + - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_method - sylius.behat.context.transform.taxon @@ -42,7 +44,12 @@ default: - sylius.behat.context.setup.taxonomy - sylius.behat.context.setup.zone + - sylius.behat.context.api.admin.managing_product_associations + - sylius.behat.context.api.admin.managing_product_images - sylius.behat.context.api.admin.managing_products + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + - Sylius\Behat\Context\Api\Admin\ManagingProductTaxonsContext filters: tags: "@managing_products&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_price_history.yml b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_price_history.yml new file mode 100644 index 0000000000..0b6e6e7b57 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_price_history.yml @@ -0,0 +1,28 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_viewing_price_history: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.shared_storage + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.admin_user + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.product + - Sylius\Behat\Context\Setup\CatalogPromotionContext + + - sylius.behat.context.api.admin.managing_product_variants + - sylius.behat.context.api.admin.save + - Sylius\Behat\Context\Api\Admin\ChannelPricingLogEntryContext + + filters: + tags: "@viewing_price_history&&@api" + javascript: false diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_price_history_after_catalog_promotions.yml b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_price_history_after_catalog_promotions.yml new file mode 100644 index 0000000000..c6d84d5ba7 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_price_history_after_catalog_promotions.yml @@ -0,0 +1,28 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_viewing_price_history_after_catalog_promotions: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.shared_storage + - Sylius\Behat\Context\Transform\CatalogPromotionContext + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.admin_user + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.product + - Sylius\Behat\Context\Setup\CatalogPromotionContext + + - sylius.behat.context.api.admin.managing_catalog_promotions + - Sylius\Behat\Context\Api\Admin\ChannelPricingLogEntryContext + + filters: + tags: "@viewing_price_history_after_catalog_promotions&&@api" + javascript: false diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml new file mode 100644 index 0000000000..0d795243ba --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_in_admin_panel.yaml @@ -0,0 +1,46 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_viewing_product_in_admin_panel: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.currency + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.locale + - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_association_type + - sylius.behat.context.transform.product_attribute + - sylius.behat.context.transform.product_option + - sylius.behat.context.transform.product_option_value + - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_category + - sylius.behat.context.transform.tax_category + - sylius.behat.context.transform.taxon + - Sylius\Behat\Context\Transform\CatalogPromotionContext + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.locale + - sylius.behat.context.setup.product + - sylius.behat.context.setup.product_association + - sylius.behat.context.setup.product_attribute + - sylius.behat.context.setup.product_option + - sylius.behat.context.setup.product_taxon + - sylius.behat.context.setup.shipping_category + - sylius.behat.context.setup.taxation + - sylius.behat.context.setup.taxonomy + - Sylius\Behat\Context\Setup\CatalogPromotionContext + - Sylius\Behat\Context\Setup\PriceHistoryContext + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext + + - sylius.behat.context.api.admin.managing_products + - Sylius\Behat\Context\Api\Admin\BrowsingCatalogPromotionProductVariantsContext + + filters: + tags: "@viewing_product_in_admin_panel&&@api" + javascript: false diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_variants.yml b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_variants.yml index 4c89f269bb..538ed42413 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_variants.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_product_variants.yml @@ -8,7 +8,10 @@ default: - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.transform.channel + - sylius.behat.context.transform.lexical - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_option + - sylius.behat.context.transform.product_option_value - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage diff --git a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml index 781f7387f8..0151bc3b83 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/product/viewing_products.yml @@ -12,9 +12,11 @@ default: - sylius.behat.context.transform.locale - sylius.behat.context.transform.product - sylius.behat.context.transform.product_association_type + - sylius.behat.context.transform.product_option_value - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.taxon + - Sylius\Behat\Context\Transform\CatalogPromotionContext - sylius.behat.context.setup.admin_user - sylius.behat.context.setup.channel @@ -25,14 +27,17 @@ default: - sylius.behat.context.setup.product_attribute - sylius.behat.context.setup.product_review - sylius.behat.context.setup.product_taxon - - sylius.behat.context.setup.shop_security + - sylius.behat.context.setup.shop_api_security - sylius.behat.context.setup.taxonomy - Sylius\Behat\Context\Setup\CatalogPromotionContext + - Sylius\Behat\Context\Setup\PriceHistoryContext + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext - sylius.behat.context.api.shop.channel - sylius.behat.context.api.shop.product - sylius.behat.context.api.shop.product_attribute - sylius.behat.context.api.shop.product_variant + - Sylius\Behat\Context\Api\Shop\TaxonContext filters: tags: "@viewing_products&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/promotion/applying_catalog_promotions.yml b/src/Sylius/Behat/Resources/config/suites/api/promotion/applying_catalog_promotions.yml index a9e87e89c7..eedb5128b8 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/promotion/applying_catalog_promotions.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/promotion/applying_catalog_promotions.yml @@ -25,10 +25,13 @@ default: - Sylius\Behat\Context\Transform\CatalogPromotionContext - sylius.behat.context.api.admin.managing_catalog_promotions - - sylius.behat.context.api.admin.managing_product_variants + - sylius.behat.context.api.admin.save - sylius.behat.context.api.shop.login - sylius.behat.context.api.shop.product - sylius.behat.context.api.shop.product_variant + - Sylius\Behat\Context\Api\Admin\CreatingProductVariantContext + - Sylius\Behat\Context\Api\Admin\ManagingProductVariantsPricesContext + - Sylius\Behat\Context\Api\Admin\ManagingProductTaxonsContext filters: tags: "@applying_catalog_promotions&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_catalog_promotions.yml b/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_catalog_promotions.yml index 4d52bade54..a48553f688 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_catalog_promotions.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_catalog_promotions.yml @@ -25,6 +25,8 @@ default: - Sylius\Behat\Context\Setup\CatalogPromotionContext - sylius.behat.context.api.admin.managing_catalog_promotions + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save - sylius.behat.context.api.shop.product_variant - Sylius\Behat\Context\Api\Admin\BrowsingCatalogPromotionProductVariantsContext diff --git a/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_promotion_coupons.yml b/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_promotion_coupons.yml new file mode 100644 index 0000000000..37509d15ec --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_promotion_coupons.yml @@ -0,0 +1,35 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_managing_promotion_coupons: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.coupon + - sylius.behat.context.transform.address + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.date_time + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.promotion + - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.shared_storage + + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.promotion + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.order + - sylius.behat.context.setup.admin_api_security + + - sylius.behat.context.api.admin.managing_promotion_coupons + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + + filters: + tags: "@managing_promotion_coupons&&@api" + javascript: false diff --git a/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_promotions.yml b/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_promotions.yml index a1ac8a94cf..c530c0b5e5 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_promotions.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/promotion/managing_promotions.yml @@ -7,19 +7,37 @@ default: contexts: - sylius.behat.context.hook.doctrine_orm + - sylius.behat.context.transform.address - sylius.behat.context.transform.channel + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.customer_group + - sylius.behat.context.transform.date_time - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.locale + - sylius.behat.context.transform.payment - sylius.behat.context.transform.product - sylius.behat.context.transform.promotion - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_method + - sylius.behat.context.transform.taxon - sylius.behat.context.setup.channel + - sylius.behat.context.setup.customer_group + - sylius.behat.context.setup.locale + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment - sylius.behat.context.setup.product + - sylius.behat.context.setup.product_taxon - sylius.behat.context.setup.promotion + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.taxonomy - sylius.behat.context.setup.admin_api_security - sylius.behat.context.api.admin.managing_promotions + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save - Sylius\Behat\Context\Api\Admin\RemovingProductContext + - Sylius\Behat\Context\Api\Admin\RemovingTaxonContext filters: tags: "@managing_promotions&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/promotion/receiving_discount.yml b/src/Sylius/Behat/Resources/config/suites/api/promotion/receiving_discount.yml index 245216938a..f495e12771 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/promotion/receiving_discount.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/promotion/receiving_discount.yml @@ -15,6 +15,7 @@ default: - sylius.behat.context.transform.payment - sylius.behat.context.transform.product - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.promotion - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.taxon - sylius.behat.context.transform.shipping_method diff --git a/src/Sylius/Behat/Resources/config/suites/api/shipping/managing_shipping_categories.yml b/src/Sylius/Behat/Resources/config/suites/api/shipping/managing_shipping_categories.yml index 0c948feafc..f88c52672a 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/shipping/managing_shipping_categories.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/shipping/managing_shipping_categories.yml @@ -15,6 +15,8 @@ default: - sylius.behat.context.transform.shipping_category - sylius.behat.context.api.admin.managing_shipping_categories + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_shipping_categories&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/shipping/managing_shipping_methods.yml b/src/Sylius/Behat/Resources/config/suites/api/shipping/managing_shipping_methods.yml index adb74b37f3..f75741fb2c 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/shipping/managing_shipping_methods.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/shipping/managing_shipping_methods.yml @@ -29,6 +29,8 @@ default: - sylius.behat.context.transform.zone - sylius.behat.context.api.admin.managing_shipping_methods + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_shipping_methods&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/taxation/managing_tax_categories.yml b/src/Sylius/Behat/Resources/config/suites/api/taxation/managing_tax_categories.yml index f090c55161..8714e55f25 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/taxation/managing_tax_categories.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/taxation/managing_tax_categories.yml @@ -14,6 +14,8 @@ default: - sylius.behat.context.setup.taxation - sylius.behat.context.api.admin.managing_tax_categories + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_tax_categories&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/taxation/managing_tax_rates.yml b/src/Sylius/Behat/Resources/config/suites/api/taxation/managing_tax_rates.yml new file mode 100644 index 0000000000..cbf4f29d03 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/taxation/managing_tax_rates.yml @@ -0,0 +1,25 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_managing_tax_rates: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.tax_category + - sylius.behat.context.transform.tax_rate + - sylius.behat.context.transform.zone + + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.taxation + - sylius.behat.context.setup.zone + + - sylius.behat.context.api.admin.managing_tax_rates + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + + filters: + tags: "@managing_tax_rates&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/taxon/managing_taxons.yml b/src/Sylius/Behat/Resources/config/suites/api/taxon/managing_taxons.yml index 056f2e6da7..19b9eb77ba 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/taxon/managing_taxons.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/taxon/managing_taxons.yml @@ -7,17 +7,23 @@ default: contexts: - sylius.behat.context.hook.doctrine_orm + - sylius.behat.context.transform.channel - sylius.behat.context.transform.locale + - sylius.behat.context.transform.product - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.taxon + - sylius.behat.context.setup.admin_api_security - sylius.behat.context.setup.channel - sylius.behat.context.setup.locale - - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.product + - sylius.behat.context.setup.product_taxon - sylius.behat.context.setup.taxonomy + - sylius.behat.context.api.admin.managing_taxon_images - sylius.behat.context.api.admin.managing_taxons - + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_taxons&&@api" javascript: false diff --git a/src/Sylius/Behat/Resources/config/suites/api/taxon/managing_taxons_ajax.yml b/src/Sylius/Behat/Resources/config/suites/api/taxon/managing_taxons_ajax.yml new file mode 100644 index 0000000000..f1c8c574ad --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/taxon/managing_taxons_ajax.yml @@ -0,0 +1,20 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + managing_taxons_ajax: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.taxon + + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.taxonomy + - sylius.behat.context.setup.admin_security + + - sylius.behat.context.api.admin.ajax_taxons + filters: + tags: "@managing_taxons_ajax&&@api" + javascript: false diff --git a/src/Sylius/Behat/Resources/config/suites/api/user/managing_administrators.yml b/src/Sylius/Behat/Resources/config/suites/api/user/managing_administrators.yml index c1fec4222c..8c5cd7006e 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/user/managing_administrators.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/user/managing_administrators.yml @@ -17,6 +17,8 @@ default: - sylius.behat.context.api.admin.login - sylius.behat.context.api.admin.managing_administrators + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_administrators&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/user/managing_customer_groups.yml b/src/Sylius/Behat/Resources/config/suites/api/user/managing_customer_groups.yml index 64cd322649..7a0897974c 100644 --- a/src/Sylius/Behat/Resources/config/suites/api/user/managing_customer_groups.yml +++ b/src/Sylius/Behat/Resources/config/suites/api/user/managing_customer_groups.yml @@ -14,6 +14,8 @@ default: - sylius.behat.context.setup.customer_group - sylius.behat.context.api.admin.managing_customer_groups + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save filters: tags: "@managing_customer_groups&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/api/user/managing_customers.yml b/src/Sylius/Behat/Resources/config/suites/api/user/managing_customers.yml new file mode 100644 index 0000000000..72123c4ad2 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/user/managing_customers.yml @@ -0,0 +1,38 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_managing_customers: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.address + - sylius.behat.context.transform.country + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.customer_group + - sylius.behat.context.transform.payment + - sylius.behat.context.transform.product + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_method + + - sylius.behat.context.setup.address + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.customer + - sylius.behat.context.setup.customer_group + - sylius.behat.context.setup.geographical + - sylius.behat.context.setup.order + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.shipping + + - sylius.behat.context.api.admin.managing_customers + - sylius.behat.context.api.admin.response + - sylius.behat.context.api.admin.save + + - sylius.behat.context.api.shop.login + + filters: + tags: "@managing_customers&&@api" + javascript: false diff --git a/src/Sylius/Behat/Resources/config/suites/api/user/managing_users.yml b/src/Sylius/Behat/Resources/config/suites/api/user/managing_users.yml new file mode 100644 index 0000000000..913ff7c923 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/api/user/managing_users.yml @@ -0,0 +1,25 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + api_managing_users: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shop_user + + - sylius.behat.context.setup.admin_api_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.customer + - sylius.behat.context.setup.user + + - sylius.behat.context.api.admin.managing_customers + - sylius.behat.context.api.admin.response + + - sylius.behat.context.api.shop.login + + filters: + tags: "@managing_users&&@api" diff --git a/src/Sylius/Behat/Resources/config/suites/cli/change_admin_password.yml b/src/Sylius/Behat/Resources/config/suites/cli/change_admin_password.yml new file mode 100644 index 0000000000..32e013d821 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/cli/change_admin_password.yml @@ -0,0 +1,15 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + change_admin_password: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.setup.admin_user + + - sylius.behat.context.cli.change_admin_password + + filters: + tags: "@change_admin_password&&@cli" diff --git a/src/Sylius/Behat/Resources/config/suites/domain/product/managing_price_history.yml b/src/Sylius/Behat/Resources/config/suites/domain/product/managing_price_history.yml new file mode 100644 index 0000000000..b2d98f4c8a --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/domain/product/managing_price_history.yml @@ -0,0 +1,22 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + domain_managing_price_history: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.product + - sylius.behat.context.transform.shared_storage + + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.product + - Sylius\Behat\Context\Setup\PriceHistoryContext + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext + + - Sylius\Behat\Context\Domain\ManagingPriceHistoryContext + filters: + tags: "@managing_price_history&&@domain" diff --git a/src/Sylius/Behat/Resources/config/suites/domain/promotion/managing_promotion_coupons.yml b/src/Sylius/Behat/Resources/config/suites/domain/promotion/managing_promotion_coupons.yml index d377ae9989..460c94b06b 100644 --- a/src/Sylius/Behat/Resources/config/suites/domain/promotion/managing_promotion_coupons.yml +++ b/src/Sylius/Behat/Resources/config/suites/domain/promotion/managing_promotion_coupons.yml @@ -28,6 +28,5 @@ default: - sylius.behat.context.domain.notification - sylius.behat.context.domain.security - filters: tags: "@managing_promotion_coupons&&@domain" diff --git a/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yml b/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml similarity index 91% rename from src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yml rename to src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml index d9db8650f1..edd74443d2 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/admin/dashboard.yaml @@ -7,7 +7,14 @@ default: contexts: - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.hook.session + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.customer + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.order + - sylius.behat.context.transform.product + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.setup.admin_security - sylius.behat.context.setup.channel - sylius.behat.context.setup.currency - sylius.behat.context.setup.geographical @@ -15,16 +22,9 @@ default: - sylius.behat.context.setup.payment - sylius.behat.context.setup.product - sylius.behat.context.setup.promotion - - sylius.behat.context.setup.admin_security - sylius.behat.context.setup.shipping - sylius.behat.context.setup.zone - - - sylius.behat.context.transform.customer - - sylius.behat.context.transform.lexical - - sylius.behat.context.transform.order - - sylius.behat.context.transform.product - - - sylius.behat.context.transform.shared_storage + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext - sylius.behat.context.ui.admin.dashboard - sylius.behat.context.ui.admin.login diff --git a/src/Sylius/Behat/Resources/config/suites/ui/checkout/checkout.yml b/src/Sylius/Behat/Resources/config/suites/ui/checkout/checkout.yml index 5a6d578dbf..808732cd01 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/checkout/checkout.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/checkout/checkout.yml @@ -21,6 +21,7 @@ default: - sylius.behat.context.transform.product - sylius.behat.context.transform.product_option - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.promotion - sylius.behat.context.transform.province - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_category diff --git a/src/Sylius/Behat/Resources/config/suites/ui/checkout/paying_for_order.yml b/src/Sylius/Behat/Resources/config/suites/ui/checkout/paying_for_order.yml index 297f5f5b42..74474c0072 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/checkout/paying_for_order.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/checkout/paying_for_order.yml @@ -21,6 +21,7 @@ default: - sylius.behat.context.transform.shipping_method - sylius.behat.context.transform.tax_category - sylius.behat.context.transform.tax_rate + - sylius.behat.context.transform.user - sylius.behat.context.transform.zone - sylius.behat.context.setup.channel @@ -37,6 +38,7 @@ default: - sylius.behat.context.setup.zone - sylius.behat.context.ui.paypal + - sylius.behat.context.ui.shop.account - sylius.behat.context.ui.shop.cart - sylius.behat.context.ui.shop.checkout - sylius.behat.context.ui.shop.checkout.addressing diff --git a/src/Sylius/Behat/Resources/config/suites/ui/locale/managing_locales.yml b/src/Sylius/Behat/Resources/config/suites/ui/locale/managing_locales.yml index c03ba0fb7e..888be1f103 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/locale/managing_locales.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/locale/managing_locales.yml @@ -14,6 +14,7 @@ default: - sylius.behat.context.setup.channel - sylius.behat.context.setup.locale - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.product - sylius.behat.context.ui.admin.managing_locales - sylius.behat.context.ui.admin.managing_translatable_entities diff --git a/src/Sylius/Behat/Resources/config/suites/ui/order/modifying_address.yml b/src/Sylius/Behat/Resources/config/suites/ui/order/modifying_placed_order_address.yaml similarity index 95% rename from src/Sylius/Behat/Resources/config/suites/ui/order/modifying_address.yml rename to src/Sylius/Behat/Resources/config/suites/ui/order/modifying_placed_order_address.yaml index acb8cda5eb..6541042c0f 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/order/modifying_address.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/order/modifying_placed_order_address.yaml @@ -3,7 +3,7 @@ default: suites: - ui_modifying_address: + ui_modifying_placed_order_address: contexts: - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.hook.session @@ -48,4 +48,4 @@ default: - sylius.behat.context.ui.shop.currency filters: - tags: "@modifying_address&&@ui" + tags: "@modifying_placed_order_address&&@ui" diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/accessing_price_history.yml b/src/Sylius/Behat/Resources/config/suites/ui/product/accessing_price_history.yml new file mode 100644 index 0000000000..474eaaa73e --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/accessing_price_history.yml @@ -0,0 +1,27 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + ui_accessing_price_history: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.currency + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.product + - sylius.behat.context.transform.shared_storage + - Sylius\Behat\Context\Transform\CatalogPromotionContext + + - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.admin_user + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.product + - Sylius\Behat\Context\Setup\CatalogPromotionContext + + - sylius.behat.context.ui.admin.product_showpage + - Sylius\Behat\Context\Ui\Admin\ChannelPricingLogEntryContext + + filters: + tags: "@accessing_price_history&&@ui" diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/managing_product_variants.yml b/src/Sylius/Behat/Resources/config/suites/ui/product/managing_product_variants.yml index d04aa5e9ad..e09d123748 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/product/managing_product_variants.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/managing_product_variants.yml @@ -13,6 +13,7 @@ default: - sylius.behat.context.transform.locale - sylius.behat.context.transform.lexical - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_option_value - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.channel @@ -33,12 +34,14 @@ default: - sylius.behat.context.setup.product - sylius.behat.context.setup.admin_security - sylius.behat.context.setup.taxonomy + - Sylius\Behat\Context\Setup\CatalogPromotionContext - sylius.behat.context.ui.admin.browsing_product_variants - sylius.behat.context.ui.admin.managing_administrator_locale - sylius.behat.context.ui.admin.managing_product_variants - sylius.behat.context.ui.admin.notification - sylius.behat.context.ui.shop.browsing_product + - Sylius\Behat\Context\Ui\Admin\ChannelPricingLogEntryContext filters: tags: "@managing_product_variants&&@ui" diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/managing_products.yml b/src/Sylius/Behat/Resources/config/suites/ui/product/managing_products.yml index 507d654682..d062ed2387 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/product/managing_products.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/managing_products.yml @@ -5,6 +5,7 @@ default: suites: ui_managing_products: contexts: + - sylius.behat.context.hook.cache - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.hook.session @@ -19,6 +20,7 @@ default: - sylius.behat.context.transform.product - sylius.behat.context.transform.product_association_type - sylius.behat.context.transform.product_option + - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_method - sylius.behat.context.transform.taxon @@ -48,6 +50,7 @@ default: - sylius.behat.context.ui.admin.managing_products - sylius.behat.context.ui.admin.notification - sylius.behat.context.ui.shop.browsing_product + - Sylius\Behat\Context\Ui\Admin\ManagingProductTaxonsContext - Sylius\Behat\Context\Ui\Admin\RemovingProductContext filters: diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_price_history.yaml b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_price_history.yaml new file mode 100644 index 0000000000..e6f8300141 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_price_history.yaml @@ -0,0 +1,27 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + ui_viewing_price_history: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.shared_storage + - Sylius\Behat\Context\Transform\CatalogPromotionContext + + - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.admin_user + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.product + - Sylius\Behat\Context\Setup\CatalogPromotionContext + + - sylius.behat.context.ui.admin.managing_product_variants + - Sylius\Behat\Context\Ui\Admin\ChannelPricingLogEntryContext + + filters: + tags: "@viewing_price_history&&@ui" diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_price_history_after_catalog_promotions.yaml b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_price_history_after_catalog_promotions.yaml new file mode 100644 index 0000000000..51c9878482 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_price_history_after_catalog_promotions.yaml @@ -0,0 +1,28 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + ui_viewing_price_history_after_catalog_promotions: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.shared_storage + - Sylius\Behat\Context\Transform\CatalogPromotionContext + + - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.admin_user + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.product + - Sylius\Behat\Context\Setup\CatalogPromotionContext + + - sylius.behat.context.ui.admin.product_showpage + - Sylius\Behat\Context\Ui\Admin\ManagingCatalogPromotionsContext + - Sylius\Behat\Context\Ui\Admin\ChannelPricingLogEntryContext + + filters: + tags: "@viewing_price_history_after_catalog_promotions&&@ui" diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml new file mode 100644 index 0000000000..71fb8bc930 --- /dev/null +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_product_in_admin_panel.yaml @@ -0,0 +1,46 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + +default: + suites: + ui_viewing_product_in_admin_panel: + contexts: + - sylius.behat.context.hook.doctrine_orm + - sylius.behat.context.hook.session + + - sylius.behat.context.transform.channel + - sylius.behat.context.transform.currency + - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.locale + - sylius.behat.context.transform.product + - sylius.behat.context.transform.product_association_type + - sylius.behat.context.transform.product_option + - sylius.behat.context.transform.product_option_value + - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.shared_storage + - sylius.behat.context.transform.shipping_category + - sylius.behat.context.transform.tax_category + - sylius.behat.context.transform.taxon + - Sylius\Behat\Context\Transform\CatalogPromotionContext + + - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.locale + - sylius.behat.context.setup.product + - sylius.behat.context.setup.product_association + - sylius.behat.context.setup.product_attribute + - sylius.behat.context.setup.product_option + - sylius.behat.context.setup.product_taxon + - sylius.behat.context.setup.shipping_category + - sylius.behat.context.setup.taxation + - sylius.behat.context.setup.taxonomy + - Sylius\Behat\Context\Setup\CatalogPromotionContext + - Sylius\Behat\Context\Setup\PriceHistoryContext + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext + + - sylius.behat.context.ui.admin.managing_product_attributes + - sylius.behat.context.ui.admin.product_showpage + - sylius.behat.context.ui.shop.browsing_product + + filters: + tags: "@viewing_product_in_admin_panel&&@ui" diff --git a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_products.yml b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_products.yml index a62a989103..98381565c0 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_products.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/product/viewing_products.yml @@ -16,6 +16,7 @@ default: - sylius.behat.context.transform.product - sylius.behat.context.transform.product_association_type - sylius.behat.context.transform.product_option + - sylius.behat.context.transform.product_option_value - sylius.behat.context.transform.product_variant - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.shipping_category @@ -40,9 +41,9 @@ default: - sylius.behat.context.setup.taxation - sylius.behat.context.setup.product_option - Sylius\Behat\Context\Setup\CatalogPromotionContext + - Sylius\Behat\Context\Setup\PriceHistoryContext + - Sylius\Calendar\Tests\Behat\Context\Setup\CalendarContext - - sylius.behat.context.ui.admin.managing_product_attributes - - sylius.behat.context.ui.admin.product_showpage - sylius.behat.context.ui.channel - sylius.behat.context.ui.shop.locale - sylius.behat.context.ui.shop.product diff --git a/src/Sylius/Behat/Resources/config/suites/ui/promotion/managing_promotions.yml b/src/Sylius/Behat/Resources/config/suites/ui/promotion/managing_promotions.yml index 06f425caa4..64d6992da2 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/promotion/managing_promotions.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/promotion/managing_promotions.yml @@ -13,6 +13,7 @@ default: - sylius.behat.context.transform.customer - sylius.behat.context.transform.date_time - sylius.behat.context.transform.lexical + - sylius.behat.context.transform.locale - sylius.behat.context.transform.payment - sylius.behat.context.transform.product - sylius.behat.context.transform.promotion @@ -23,6 +24,7 @@ default: - sylius.behat.context.setup.channel - sylius.behat.context.setup.customer_group - sylius.behat.context.setup.currency + - sylius.behat.context.setup.locale - sylius.behat.context.setup.order - sylius.behat.context.setup.payment - sylius.behat.context.setup.product diff --git a/src/Sylius/Behat/Resources/config/suites/ui/promotion/receiving_discount.yml b/src/Sylius/Behat/Resources/config/suites/ui/promotion/receiving_discount.yml index 4df4b04cb6..0ddfca73ee 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/promotion/receiving_discount.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/promotion/receiving_discount.yml @@ -13,6 +13,7 @@ default: - sylius.behat.context.transform.payment - sylius.behat.context.transform.product - sylius.behat.context.transform.product_variant + - sylius.behat.context.transform.promotion - sylius.behat.context.transform.shared_storage - sylius.behat.context.transform.taxon diff --git a/src/Sylius/Behat/Resources/config/suites/ui/taxonomy/managing_taxons.yml b/src/Sylius/Behat/Resources/config/suites/ui/taxonomy/managing_taxons.yml index 506db76490..e95adb7dff 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/taxonomy/managing_taxons.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/taxonomy/managing_taxons.yml @@ -22,9 +22,8 @@ default: - sylius.behat.context.setup.taxonomy - sylius.behat.context.ui.admin.managing_taxons - - sylius.behat.context.ui.admin.removing_taxons - sylius.behat.context.ui.admin.notification - - sylius.behat.context.ui.shop.product + - sylius.behat.context.ui.admin.removing_taxons filters: tags: "@managing_taxons&&@ui" diff --git a/src/Sylius/Behat/Resources/config/suites/ui/user/managing_customers.yml b/src/Sylius/Behat/Resources/config/suites/ui/user/managing_customers.yml index 390d93a508..7494a1b012 100644 --- a/src/Sylius/Behat/Resources/config/suites/ui/user/managing_customers.yml +++ b/src/Sylius/Behat/Resources/config/suites/ui/user/managing_customers.yml @@ -5,6 +5,7 @@ default: suites: ui_managing_customers: contexts: + - sylius.behat.context.hook.cache - sylius.behat.context.hook.doctrine_orm - sylius.behat.context.hook.session diff --git a/src/Sylius/Behat/Service/AutocompleteHelper.php b/src/Sylius/Behat/Service/AutocompleteHelper.php index e1c4a209e1..7450710cda 100644 --- a/src/Sylius/Behat/Service/AutocompleteHelper.php +++ b/src/Sylius/Behat/Service/AutocompleteHelper.php @@ -46,6 +46,19 @@ abstract class AutocompleteHelper static::waitForElementToBeVisible($session, $element); } + public static function removeValue(Session $session, NodeElement $element, string $value): void + { + $session->wait(3000, sprintf( + '$(document.evaluate("%s", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).dropdown("is visible")', + str_replace('"', '\"', $element->getXpath()), + )); + + $elementToRemove = $element->find('css', sprintf('a.ui.label:contains("%s")', $value)); + $elementToRemove->find('css', 'i.delete')->click(); + + JQueryHelper::waitForAsynchronousActionsToFinish($session); + } + public static function isValueVisible(Session $session, NodeElement $element, $value): bool { static::activateAutocompleteDropdown($session, $element); diff --git a/src/Sylius/Behat/Service/Checker/EmailChecker.php b/src/Sylius/Behat/Service/Checker/EmailChecker.php index e106ad4d4e..11b3bfc4c3 100644 --- a/src/Sylius/Behat/Service/Checker/EmailChecker.php +++ b/src/Sylius/Behat/Service/Checker/EmailChecker.php @@ -13,20 +13,19 @@ declare(strict_types=1); namespace Sylius\Behat\Service\Checker; -use Psr\Cache\CacheItemPoolInterface; -use Sylius\Behat\Service\MessageSendCacher; +use Sylius\Behat\Service\Provider\EmailMessagesProviderInterface; use Symfony\Component\Mime\Email; use Webmozart\Assert\Assert; final class EmailChecker implements EmailCheckerInterface { - public function __construct(private CacheItemPoolInterface $cache) + public function __construct(private EmailMessagesProviderInterface $emailMessagesProvider) { } public function hasRecipient(string $recipient): bool { - $messages = $this->getMailerMessages(); + $messages = $this->emailMessagesProvider->provide(); foreach ($messages as $email) { if ($this->isMessageTo($email, $recipient)) { @@ -41,7 +40,7 @@ final class EmailChecker implements EmailCheckerInterface { $this->assertRecipientIsValid($recipient); - $messages = $this->getMailerMessages(); + $messages = $this->emailMessagesProvider->provide(); foreach ($messages as $email) { if ($this->isMessageTo($email, $recipient)) { @@ -61,7 +60,7 @@ final class EmailChecker implements EmailCheckerInterface $this->assertRecipientIsValid($recipient); $messagesCount = 0; - $messages = $this->getMailerMessages(); + $messages = $this->emailMessagesProvider->provide(); foreach ($messages as $email) { if ($this->isMessageTo($email, $recipient)) { @@ -95,10 +94,4 @@ final class EmailChecker implements EmailCheckerInterface 'Given recipient is not a valid email address.', ); } - - /** @return Email[] */ - private function getMailerMessages(): array - { - return $this->cache->hasItem(MessageSendCacher::CACHE_KEY) ? $this->cache->getItem(MessageSendCacher::CACHE_KEY)->get() : []; - } } diff --git a/src/Sylius/Behat/Service/Converter/IriConverter.php b/src/Sylius/Behat/Service/Converter/IriConverter.php index ba3b46aac4..dec95fba50 100644 --- a/src/Sylius/Behat/Service/Converter/IriConverter.php +++ b/src/Sylius/Behat/Service/Converter/IriConverter.php @@ -1,9 +1,9 @@ + * (c) Sylius Sp. z o.o. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -13,206 +13,72 @@ declare(strict_types=1); namespace Sylius\Behat\Service\Converter; -use ApiPlatform\Core\Api\IdentifiersExtractor; -use ApiPlatform\Core\Api\IdentifiersExtractorInterface; -use ApiPlatform\Core\Api\IriConverterInterface; -use ApiPlatform\Core\Api\OperationType; -use ApiPlatform\Core\Api\ResourceClassResolverInterface; -use ApiPlatform\Core\Api\UrlGeneratorInterface; -use ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameResolverInterface; -use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; -use ApiPlatform\Core\DataProvider\OperationDataProviderTrait; -use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; -use ApiPlatform\Core\Exception\InvalidArgumentException; -use ApiPlatform\Core\Exception\InvalidIdentifierException; -use ApiPlatform\Core\Exception\ItemNotFoundException; -use ApiPlatform\Core\Exception\RuntimeException; -use ApiPlatform\Core\Identifier\CompositeIdentifierParser; -use ApiPlatform\Core\Identifier\IdentifierConverterInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; -use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; -use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; -use ApiPlatform\Core\Util\AttributesExtractor; -use ApiPlatform\Core\Util\ResourceClassInfoTrait; -use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface; -use Symfony\Component\Routing\RouterInterface; +use ApiPlatform\Api\IriConverterInterface; +use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Core\Api\IriConverterInterface as LegacyIriConverterInterface; +use ApiPlatform\Metadata\Operation; /** - * Copied from src/Bridge/Symfony/Routing/IriConverter.php to add getIriFromItemInSection method + * Wrapper between the legacy iri converter and the new one + * + * @experimental */ -final class IriConverter implements IriConverterInterface +final class IriConverter implements LegacyIriConverterInterface, IriConverterInterface { - use OperationDataProviderTrait; - use ResourceClassInfoTrait; - - private $routeNameResolver; - private $router; - private $identifiersExtractor; - public function __construct( - PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, - PropertyMetadataFactoryInterface $propertyMetadataFactory, - ItemDataProviderInterface $itemDataProvider, - RouteNameResolverInterface $routeNameResolver, - RouterInterface $router, - PropertyAccessorInterface $propertyAccessor = null, - IdentifiersExtractorInterface $identifiersExtractor = null, - SubresourceDataProviderInterface $subresourceDataProvider = null, - IdentifierConverterInterface $identifierConverter = null, - ResourceClassResolverInterface $resourceClassResolver = null, - ResourceMetadataFactoryInterface $resourceMetadataFactory = null + private LegacyIriConverterInterface $legacyIriConverter, + private IriConverterInterface $iriConverter, ) { - $this->itemDataProvider = $itemDataProvider; - $this->routeNameResolver = $routeNameResolver; - $this->router = $router; - $this->subresourceDataProvider = $subresourceDataProvider; - $this->identifierConverter = $identifierConverter; - $this->resourceClassResolver = $resourceClassResolver; - $this->identifiersExtractor = $identifiersExtractor ?: new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor ?? PropertyAccess::createPropertyAccessor()); - $this->resourceMetadataFactory = $resourceMetadataFactory; } - /** - * {@inheritdoc} - * - * @return object - */ public function getItemFromIri(string $iri, array $context = []) { - try { - $parameters = $this->router->match($iri); - } catch (RoutingExceptionInterface $e) { - throw new InvalidArgumentException(sprintf('No route matches "%s".', $iri), $e->getCode(), $e); - } - - if (!isset($parameters['_api_resource_class'])) { - throw new InvalidArgumentException(sprintf('No resource associated to "%s".', $iri)); - } - - if (isset($parameters['_api_collection_operation_name'])) { - throw new InvalidArgumentException(sprintf('The iri "%s" references a collection not an item.', $iri)); - } - - $attributes = AttributesExtractor::extractAttributes($parameters); - - try { - $identifiers = $this->extractIdentifiers($parameters, $attributes); - } catch (InvalidIdentifierException $e) { - throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); - } - - if ($this->identifierConverter) { - $context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] = true; - } - - if (isset($attributes['subresource_operation_name'])) { - if (($item = $this->getSubresourceData($identifiers, $attributes, $context)) && !\is_array($item)) { - return $item; - } - - throw new ItemNotFoundException(sprintf('Item not found for "%s".', $iri)); - } - - if ($item = $this->getItemData($identifiers, $attributes, $context)) { - return $item; - } - - throw new ItemNotFoundException(sprintf('Item not found for "%s".', $iri)); + return $this->legacyIriConverter->getItemFromIri($iri, $context); } - /** - * {@inheritdoc} - */ - public function getIriFromItem($item, int $referenceType = null): string + public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface::ABS_PATH): string { - $resourceClass = $this->getResourceClass($item, true); + return $this->legacyIriConverter->getIriFromItem($item, $referenceType); + } - try { - $identifiers = $this->identifiersExtractor->getIdentifiersFromItem($item); - } catch (RuntimeException $e) { - throw new InvalidArgumentException(sprintf('Unable to generate an IRI for the item of type "%s"', $resourceClass), $e->getCode(), $e); - } + public function getIriFromResourceClass( + string $resourceClass, + int $referenceType = UrlGeneratorInterface::ABS_PATH, + ): string { + return $this->legacyIriConverter->getIriFromResourceClass($resourceClass, $referenceType); + } - return $this->getItemIriFromResourceClass( + public function getItemIriFromResourceClass( + string $resourceClass, + array $identifiers, + int $referenceType = UrlGeneratorInterface::ABS_PATH, + ): string { + return $this->legacyIriConverter->getItemIriFromResourceClass($resourceClass, $identifiers, $referenceType); + } + + public function getSubresourceIriFromResourceClass( + string $resourceClass, + array $identifiers, + int $referenceType = UrlGeneratorInterface::ABS_PATH, + ): string { + return $this->legacyIriConverter->getSubresourceIriFromResourceClass( $resourceClass, $identifiers, - $this->getReferenceType($resourceClass, $referenceType) + $referenceType, ); } - /** - * {@inheritdoc} - */ - public function getIriFromItemInSection($item, string $section, int $referenceType = null): string + public function getResourceFromIri(string $iri, array $context = [], ?Operation $operation = null): object { - $resourceClass = $this->getResourceClass($item, true); - - try { - $identifiers = $this->identifiersExtractor->getIdentifiersFromItem($item); - } catch (RuntimeException $e) { - throw new InvalidArgumentException(sprintf('Unable to generate an IRI for the item of type "%s"', $resourceClass), $e->getCode(), $e); - } - - return $this->getItemIriFromResourceClass( - $resourceClass, - $identifiers, - $this->getReferenceType($resourceClass, $referenceType), - $section - ); + return $this->iriConverter->getResourceFromIri($iri, $context, $operation); } - /** - * {@inheritdoc} - */ - public function getIriFromResourceClass(string $resourceClass, int $referenceType = null): string - { - try { - return $this->router->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::COLLECTION), [], $this->getReferenceType($resourceClass, $referenceType)); - } catch (RoutingExceptionInterface $e) { - throw new InvalidArgumentException(sprintf('Unable to generate an IRI for "%s".', $resourceClass), $e->getCode(), $e); - } - } - - /** - * {@inheritdoc} - */ - public function getItemIriFromResourceClass(string $resourceClass, array $identifiers, int $referenceType = null, string $section = null): string - { - $routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM, $section ? ['section' => $section] : []); - $metadata = $this->resourceMetadataFactory->create($resourceClass); - - if (\count($identifiers) > 1 && true === $metadata->getAttribute('composite_identifier', true)) { - $identifiers = ['id' => CompositeIdentifierParser::stringify($identifiers)]; - } - - try { - return $this->router->generate($routeName, $identifiers, $this->getReferenceType($resourceClass, $referenceType)); - } catch (RoutingExceptionInterface $e) { - throw new InvalidArgumentException(sprintf('Unable to generate an IRI for "%s".', $resourceClass), $e->getCode(), $e); - } - } - - /** - * {@inheritdoc} - */ - public function getSubresourceIriFromResourceClass(string $resourceClass, array $context, int $referenceType = null): string - { - try { - return $this->router->generate($this->routeNameResolver->getRouteName($resourceClass, OperationType::SUBRESOURCE, $context), $context['subresource_identifiers'], $this->getReferenceType($resourceClass, $referenceType)); - } catch (RoutingExceptionInterface $e) { - throw new InvalidArgumentException(sprintf('Unable to generate an IRI for "%s".', $resourceClass), $e->getCode(), $e); - } - } - - private function getReferenceType(string $resourceClass, ?int $referenceType): ?int - { - if (null === $referenceType && null !== $this->resourceMetadataFactory) { - $metadata = $this->resourceMetadataFactory->create($resourceClass); - $referenceType = $metadata->getAttribute('url_generation_strategy'); - } - - return $referenceType ?? UrlGeneratorInterface::ABS_PATH; + public function getIriFromResource( + $resource, + int $referenceType = UrlGeneratorInterface::ABS_PATH, + ?Operation $operation = null, + array $context = [], + ): ?string { + return $this->iriConverter->getIriFromResource($resource, $referenceType, $operation, $context); } } diff --git a/src/Sylius/Behat/Service/Converter/SectionAwareIriConverter.php b/src/Sylius/Behat/Service/Converter/SectionAwareIriConverter.php new file mode 100644 index 0000000000..e462211ca1 --- /dev/null +++ b/src/Sylius/Behat/Service/Converter/SectionAwareIriConverter.php @@ -0,0 +1,89 @@ +identifiersExtractor = $identifiersExtractor ?: new IdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor ?? PropertyAccess::createPropertyAccessor()); + $this->resourceClassResolver = $resourceClassResolver; + $this->resourceMetadataFactory = $resourceMetadataFactory; + } + + public function getIriFromResourceInSection(object $item, string $section, int $referenceType = null): string + { + $resourceClass = $this->getResourceClass($item, true); + + try { + $identifiers = $this->identifiersExtractor->getIdentifiersFromItem($item); + } catch (RuntimeException $e) { + throw new InvalidArgumentException(sprintf('Unable to generate an IRI for the item of type "%s"', $resourceClass), $e->getCode(), $e); + } + + $routeName = $this->routeNameResolver->getRouteName($resourceClass, OperationType::ITEM, $section ? ['section' => $section] : []); + $metadata = $this->resourceMetadataFactory->create($resourceClass); + + if (\count($identifiers) > 1 && true === $metadata->getAttribute('composite_identifier', true)) { + $identifiers = ['id' => CompositeIdentifierParser::stringify($identifiers)]; + } + + try { + return $this->router->generate($routeName, $identifiers, $this->getReferenceType($resourceClass, $referenceType)); + } catch (RoutingExceptionInterface $e) { + throw new InvalidArgumentException(sprintf('Unable to generate an IRI for "%s".', $resourceClass), $e->getCode(), $e); + } + } + + private function getReferenceType(string $resourceClass, ?int $referenceType): ?int + { + if (null === $referenceType && null !== $this->resourceMetadataFactory) { + $metadata = $this->resourceMetadataFactory->create($resourceClass); + $referenceType = $metadata->getAttribute('url_generation_strategy'); + } + + return $referenceType ?? UrlGeneratorInterface::ABS_PATH; + } +} diff --git a/src/Sylius/Behat/Service/Converter/SectionAwareIriConverterInterface.php b/src/Sylius/Behat/Service/Converter/SectionAwareIriConverterInterface.php new file mode 100644 index 0000000000..05fa474994 --- /dev/null +++ b/src/Sylius/Behat/Service/Converter/SectionAwareIriConverterInterface.php @@ -0,0 +1,19 @@ +cache->hasItem(MessageSendCacher::CACHE_KEY) ? $this->cache->getItem(MessageSendCacher::CACHE_KEY)->get() : []; + } +} diff --git a/src/Sylius/Behat/Service/Provider/EmailMessagesProviderInterface.php b/src/Sylius/Behat/Service/Provider/EmailMessagesProviderInterface.php new file mode 100644 index 0000000000..956ab09eaf --- /dev/null +++ b/src/Sylius/Behat/Service/Provider/EmailMessagesProviderInterface.php @@ -0,0 +1,22 @@ +mock('sylius.payum.http_client', HttpClient::class)->shouldBeCalled(); + $container->mock('sylius.payum.http_client', ClientInterface::class)->shouldBeCalled(); - $this->mockService('sylius.payum.http_client', HttpClient::class); + $this->mockService('sylius.payum.http_client', ClientInterface::class); } function it_mocks_collaborator() { - $this->mockCollaborator(HttpClient::class)->shouldHaveType(MockInterface::class); + $this->mockCollaborator(ClientInterface::class)->shouldHaveType(MockInterface::class); } } diff --git a/src/Sylius/Behat/spec/Service/Provider/EmailMessagesProviderSpec.php b/src/Sylius/Behat/spec/Service/Provider/EmailMessagesProviderSpec.php new file mode 100644 index 0000000000..ca5144c949 --- /dev/null +++ b/src/Sylius/Behat/spec/Service/Provider/EmailMessagesProviderSpec.php @@ -0,0 +1,54 @@ +beConstructedWith($cacheItemPool); + } + + function it_implements_email_messages_provider_interface(): void + { + $this->shouldImplement(EmailMessagesProviderInterface::class); + } + + function it_provides_email_messages( + CacheItemPoolInterface $cacheItemPool, + CacheItemInterface $cacheItem, + ): void { + $emailMessages = [new Email(), new Email(), new Email()]; + $cacheItem->get()->willReturn($emailMessages); + + $cacheItemPool->hasItem(MessageSendCacher::CACHE_KEY)->willReturn(true); + $cacheItemPool->getItem(MessageSendCacher::CACHE_KEY)->willReturn($cacheItem); + + $this->provide()->shouldReturn($emailMessages); + } + + function it_returns_an_empty_array_if_cache_key_does_not_exist(CacheItemPoolInterface $cacheItemPool): void + { + $cacheItemPool->hasItem(MessageSendCacher::CACHE_KEY)->willReturn(false); + + $this->provide()->shouldReturn([]); + } +} diff --git a/src/Sylius/Bundle/AddressingBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/AddressingBundle/DependencyInjection/Configuration.php index 353b73fe57..fed7c017d7 100644 --- a/src/Sylius/Bundle/AddressingBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/AddressingBundle/DependencyInjection/Configuration.php @@ -19,6 +19,7 @@ use Sylius\Bundle\AddressingBundle\Form\Type\CountryType; use Sylius\Bundle\AddressingBundle\Form\Type\ProvinceType; use Sylius\Bundle\AddressingBundle\Form\Type\ZoneMemberType; use Sylius\Bundle\AddressingBundle\Form\Type\ZoneType; +use Sylius\Bundle\AddressingBundle\Repository\ZoneRepository; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; use Sylius\Bundle\ResourceBundle\SyliusResourceBundle; use Sylius\Component\Addressing\Model\Address; @@ -142,7 +143,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->defaultValue(Zone::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(ZoneInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(ZoneRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->end() ->scalarNode('form')->defaultValue(ZoneType::class)->cannotBeEmpty()->end() ->end() diff --git a/src/Sylius/Bundle/AddressingBundle/Form/Type/ZoneChoiceType.php b/src/Sylius/Bundle/AddressingBundle/Form/Type/ZoneChoiceType.php index 4f12e3977a..983b9c04c8 100644 --- a/src/Sylius/Bundle/AddressingBundle/Form/Type/ZoneChoiceType.php +++ b/src/Sylius/Bundle/AddressingBundle/Form/Type/ZoneChoiceType.php @@ -25,7 +25,11 @@ final class ZoneChoiceType extends AbstractType public function __construct(private RepositoryInterface $zoneRepository, private array $scopeTypes = []) { if (count($scopeTypes) === 0) { - @trigger_error('Not passing scopeTypes thru constructor is deprecated in Sylius 1.5 and it will be removed in Sylius 2.0'); + trigger_deprecation( + 'sylius/addressing-bundle', + '1.5', + 'Not passing $scopeTypes through constructor is deprecated and will be prohibited in Sylius 2.0.', + ); } } diff --git a/src/Sylius/Bundle/AddressingBundle/Repository/ZoneRepository.php b/src/Sylius/Bundle/AddressingBundle/Repository/ZoneRepository.php new file mode 100644 index 0000000000..1e5b9070bd --- /dev/null +++ b/src/Sylius/Bundle/AddressingBundle/Repository/ZoneRepository.php @@ -0,0 +1,121 @@ + + */ +class ZoneRepository extends EntityRepository implements ZoneRepositoryInterface +{ + public function findOneByAddressAndType(AddressInterface $address, string $type, ?string $scope = null): ?ZoneInterface + { + $queryBuilder = $this->createByAddressQueryBuilder($address, $scope); + + $queryBuilder + ->andWhere($queryBuilder->expr()->eq('o.type', ':type')) + ->setParameter('type', $type) + ->setMaxResults(1) + ; + + return $queryBuilder->getQuery()->getOneOrNullResult(); + } + + /** @return ZoneInterface[] */ + public function findByAddress(AddressInterface $address, ?string $scope = null): array + { + return $this->createByAddressQueryBuilder($address, $scope)->getQuery()->getResult(); + } + + public function createByAddressQueryBuilder(AddressInterface $address, ?string $scope = null): QueryBuilder + { + $queryBuilder = $this->createQueryBuilder('o') + ->select('o', 'members') + ->leftJoin('o.members', 'members') + ; + + if (null !== $scope) { + $queryBuilder + ->andWhere($queryBuilder->expr()->in('o.scope', ':scopes')) + ->setParameter('scopes', array_unique([$scope, Scope::ALL])) + ; + } + + $orConditions = []; + + if ($address->getCountryCode() !== null) { + $orConditions[] = $queryBuilder->expr()->andX( + $queryBuilder->expr()->eq('o.type', ':country'), + $queryBuilder->expr()->eq('members.code', ':countryCode'), + ); + + $queryBuilder->setParameter('country', ZoneInterface::TYPE_COUNTRY); + $queryBuilder->setParameter('countryCode', $address->getCountryCode()); + } + + if ($address->getProvinceCode() !== null) { + $orConditions[] = $queryBuilder->expr()->andX( + $queryBuilder->expr()->eq('o.type', ':province'), + $queryBuilder->expr()->eq('members.code', ':provinceCode'), + ); + + $queryBuilder->setParameter('province', ZoneInterface::TYPE_PROVINCE); + $queryBuilder->setParameter('provinceCode', $address->getProvinceCode()); + } + + $queryBuilder->andWhere($queryBuilder->expr()->orX(...$orConditions)); + + return $queryBuilder; + } + + /** + * @param array $members + * + * @return array + */ + public function findByMembers(array $members, ?string $scope = null): array + { + $zonesCodes = array_map( + fn (ZoneInterface $zone): string => $zone->getCode(), + $members, + ); + + $queryBuilder = $this->createQueryBuilder('o') + ->select('o', 'members') + ->leftJoin('o.members', 'members') + ; + + if (null !== $scope) { + $queryBuilder + ->andWhere($queryBuilder->expr()->in('o.scope', ':scopes')) + ->setParameter('scopes', array_unique([$scope, Scope::ALL])) + ; + } + + $queryBuilder + ->andWhere('o.type = :type') + ->andWhere($queryBuilder->expr()->in('members.code', ':zones')) + ->setParameter('type', ZoneInterface::TYPE_ZONE) + ->setParameter('zones', $zonesCodes) + ; + + return $queryBuilder->getQuery()->getResult(); + } +} diff --git a/src/Sylius/Bundle/AddressingBundle/Resources/config/validation/Country.xml b/src/Sylius/Bundle/AddressingBundle/Resources/config/validation/Country.xml index 944710d563..5ed2adf724 100644 --- a/src/Sylius/Bundle/AddressingBundle/Resources/config/validation/Country.xml +++ b/src/Sylius/Bundle/AddressingBundle/Resources/config/validation/Country.xml @@ -19,6 +19,10 @@ + + + + @@ -29,11 +33,6 @@ - - - - - diff --git a/src/Sylius/Bundle/AddressingBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/AddressingBundle/Resources/translations/validators.en.yml index a921a020db..6ecbda28a0 100644 --- a/src/Sylius/Bundle/AddressingBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/AddressingBundle/Resources/translations/validators.en.yml @@ -1,46 +1,46 @@ sylius: address: city: - max_length: City must not be longer than {{ limit }} characters|City must not be longer than {{ limit }} characters. - min_length: City must be at least {{ limit }} characters long|City must be at least {{ limit }} characters long. + max_length: City must not be longer than {{ limit }} character.|City must not be longer than {{ limit }} characters. + min_length: City must be at least {{ limit }} characters long.|City must be at least {{ limit }} characters long. not_blank: Please enter city. country: not_blank: Please select country. disabled: Chosen country is disabled. Please choose a different one. first_name: - max_length: First name must not be longer than {{ limit }} characters|First name must not be longer than {{ limit }} characters. - min_length: First name must be at least {{ limit }} characters long|First name must be at least {{ limit }} characters long. + max_length: First name must not be longer than {{ limit }} character.|First name must not be longer than {{ limit }} characters. + min_length: First name must be at least {{ limit }} characters long.|First name must be at least {{ limit }} characters long. not_blank: Please enter first name. last_name: - max_length: Last name must not be longer than {{ limit }} characters|Last name must not be longer than {{ limit }} characters. - min_length: Last name must be at least {{ limit }} characters long|Last name must be at least {{ limit }} characters long. + max_length: Last name must not be longer than {{ limit }} character.|Last name must not be longer than {{ limit }} characters. + min_length: Last name must be at least {{ limit }} characters long.|Last name must be at least {{ limit }} characters long. not_blank: Please enter last name. postcode: - max_length: Postcode must not be longer than {{ limit }} characters|Postcode must not be longer than {{ limit }} characters. - min_length: Postcode must be at least {{ limit }} characters long|Postcode must be at least {{ limit }} characters long. + max_length: Postcode must not be longer than {{ limit }} character.|Postcode must not be longer than {{ limit }} characters. + min_length: Postcode must be at least {{ limit }} character long.|Postcode must be at least {{ limit }} characters long. not_blank: Please enter postcode. province: valid: Please select proper province. street: - max_length: Street must not be longer than {{ limit }} characters|Street must not be longer than {{ limit }} characters. - min_length: Street must be at least {{ limit }} characters long|Street must be at least {{ limit }} characters long. + max_length: Street must not be longer than {{ limit }} character.|Street must not be longer than {{ limit }} characters. + min_length: Street must be at least {{ limit }} characters long.|Street must be at least {{ limit }} characters long. not_blank: Please enter street. not_shippable: This address is not a valid shipping destination. country: code: not_blank: Please enter country ISO code. - regex: Country code can only be comprised of letters, numbers, dashes and underscores. unique: Country ISO code must be unique. + invalid: Country ISO code is invalid. unique_provinces: All provinces within this country need to have unique codes and names. province: code: - min_length: Province code must be at least 5 characters long|Province code must be at least 5 characters long. + min_length: Province code must be at least 5 characters long.|Province code must be at least 5 characters long. not_blank: Please enter province code. regex: Province code should have the following format XX-XX (e.g. US-FL). unique: Province code must be unique. name: - max_length: Province name must not be longer than {{ limit }} characters|Province name must not be longer than {{ limit }} characters. - min_length: Province name must be at least {{ limit }} characters long|Province name must be at least {{ limit }} characters long. + max_length: Province name must not be longer than {{ limit }} character.|Province name must not be longer than {{ limit }} characters. + min_length: Province name must be at least {{ limit }} characters long.|Province name must be at least {{ limit }} characters long. not_blank: Please enter province name. unique: Province name must be unique. zone: @@ -53,8 +53,8 @@ sylius: members: min_count: Please add at least {{ limit }} zone member. name: - max_length: Zone name must not be longer than {{ limit }} characters|Zone name must not be longer than {{ limit }} characters. - min_length: Zone name must be at least {{ limit }} characters long|Zone name must be at least {{ limit }} characters long. + max_length: Zone name must not be longer than {{ limit }} character.|Zone name must not be longer than {{ limit }} characters. + min_length: Zone name must be at least {{ limit }} characters long.|Zone name must be at least {{ limit }} characters long. not_blank: Please enter zone name. zone_member: cannot_be_the_same_as_zone: Zone member cannot be the same as a zone. diff --git a/src/Sylius/Bundle/AddressingBundle/Resources/translations/validators.fr.yml b/src/Sylius/Bundle/AddressingBundle/Resources/translations/validators.fr.yml index 6769bd9245..de0e739802 100644 --- a/src/Sylius/Bundle/AddressingBundle/Resources/translations/validators.fr.yml +++ b/src/Sylius/Bundle/AddressingBundle/Resources/translations/validators.fr.yml @@ -32,8 +32,8 @@ sylius: country: code: not_blank: Veuillez entrer le code ISO du pays. - regex: Le code du pays peut uniquement être constitué de lettres, chiffres, tirets et de tirets bas. unique: Le code ISO du pays doit être unique. + invalid: Code pays non valide unique_provinces: Toutes les provinces de ce pays doivent avoir des codes et des noms uniques. province: code: diff --git a/src/Sylius/Bundle/AddressingBundle/Tests/Form/Type/CountryChoiceTypeTest.php b/src/Sylius/Bundle/AddressingBundle/Tests/Form/Type/CountryChoiceTypeTest.php index a32353251a..4056a6cc98 100644 --- a/src/Sylius/Bundle/AddressingBundle/Tests/Form/Type/CountryChoiceTypeTest.php +++ b/src/Sylius/Bundle/AddressingBundle/Tests/Form/Type/CountryChoiceTypeTest.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Bundle\AddressingBundle\Tests\Form\Type; use PHPUnit\Framework\Assert; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ProphecyInterface; use Sylius\Bundle\AddressingBundle\Form\Type\CountryChoiceType; @@ -25,6 +26,8 @@ use Symfony\Component\Form\Test\TypeTestCase; final class CountryChoiceTypeTest extends TypeTestCase { + use ProphecyTrait; + private ObjectProphecy $countryRepository; /** @var ProphecyInterface|CountryInterface */ diff --git a/src/Sylius/Bundle/AddressingBundle/Tests/Form/Type/ZoneChoiceTypeTest.php b/src/Sylius/Bundle/AddressingBundle/Tests/Form/Type/ZoneChoiceTypeTest.php index d85bf2176d..a87d4fca54 100644 --- a/src/Sylius/Bundle/AddressingBundle/Tests/Form/Type/ZoneChoiceTypeTest.php +++ b/src/Sylius/Bundle/AddressingBundle/Tests/Form/Type/ZoneChoiceTypeTest.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Bundle\AddressingBundle\Tests\Form\Type; use PHPUnit\Framework\Assert; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ProphecyInterface; use Sylius\Bundle\AddressingBundle\Form\Type\ZoneChoiceType; @@ -26,6 +27,8 @@ use Symfony\Component\Form\Test\TypeTestCase; final class ZoneChoiceTypeTest extends TypeTestCase { + use ProphecyTrait; + private ObjectProphecy $zoneRepository; /** @var ProphecyInterface|ZoneInterface */ diff --git a/src/Sylius/Bundle/AddressingBundle/Tests/Matcher/ZoneMatcherTest.php b/src/Sylius/Bundle/AddressingBundle/Tests/Matcher/ZoneMatcherTest.php new file mode 100644 index 0000000000..745bb11dba --- /dev/null +++ b/src/Sylius/Bundle/AddressingBundle/Tests/Matcher/ZoneMatcherTest.php @@ -0,0 +1,85 @@ +loadFixtures(); + } + + /** @test */ + public function it_matches_all_zones_with_their_parents(): void + { + $address = new Address(); + $address->setCountryCode('PL'); + $address->setProvinceCode('MA'); + + $zoneMatcher = self::getContainer()->get('sylius.zone_matcher'); + $matchedZones = []; + + foreach ($zoneMatcher->matchAll($address) as $zone) { + $matchedZones[$zone->getCode()] = $zone; + } + + $this->assertCount(4, $matchedZones); + $this->assertArrayHasKey('EU', $matchedZones); + $this->assertArrayHasKey('VISEGRAD_GROUP', $matchedZones); + $this->assertArrayHasKey('PL', $matchedZones); + $this->assertArrayHasKey('NATO', $matchedZones); + } + + /** @test */ + public function it_matches_all_zones_with_their_parents_with_restricting_by_scope(): void + { + $address = new Address(); + $address->setCountryCode('PL'); + $address->setProvinceCode('MA'); + + $zoneMatcher = self::getContainer()->get('sylius.zone_matcher'); + $matchedZones = []; + + foreach ($zoneMatcher->matchAll($address, 'shipping') as $zone) { + $matchedZones[$zone->getCode()] = $zone; + } + + $this->assertCount(3, $matchedZones); + $this->assertArrayHasKey('NATO', $matchedZones); + $this->assertArrayHasKey('PL', $matchedZones); + $this->assertArrayHasKey('VISEGRAD_GROUP', $matchedZones); + } + + private function loadFixtures(): void + { + /** @var LoaderInterface $fixtureLoader */ + $fixtureLoader = self::getContainer()->get('fidry_alice_data_fixtures.loader.doctrine'); + + /** @var EntityManagerInterface $manager */ + $manager = self::getContainer()->get('doctrine.orm.default_entity_manager'); + + (new ORMPurger($manager))->purge(); + + $fixtureLoader->load([ + __DIR__ . '/ZoneMatcherTest/fixtures.yaml', + ], [], [], PurgeMode::createDeleteMode()); + } +} diff --git a/src/Sylius/Bundle/AddressingBundle/Tests/Matcher/ZoneMatcherTest/fixtures.yaml b/src/Sylius/Bundle/AddressingBundle/Tests/Matcher/ZoneMatcherTest/fixtures.yaml new file mode 100644 index 0000000000..1ff59f3210 --- /dev/null +++ b/src/Sylius/Bundle/AddressingBundle/Tests/Matcher/ZoneMatcherTest/fixtures.yaml @@ -0,0 +1,42 @@ +Sylius\Component\Addressing\Model\Country: + country_PL: + code: PL + +Sylius\Component\Addressing\Model\Province: + province_{MA, LD, MZ, DS}: + code: + name: + abbreviation: + country: "@country_PL" + +Sylius\Component\Addressing\Model\ZoneMember: + member_province_{MA, LD, MZ, DS}: + code: + member_{poland_visegrad_group, poland_nato}: + code: PL + member_visegrad_group: + code: "VISEGRAD_GROUP" + +Sylius\Component\Addressing\Model\Zone: + zone_poland: + code: PL + name: Poland + type: province + members: ["@member_province_MA", "@member_province_LD", "@member_province_MZ", "@member_province_DS"] + zone_visegrad_group: + code: VISEGRAD_GROUP + name: Visegrad Group + type: zone + members: ["@member_poland_visegrad_group"] + zone_eu: + code: EU + name: European Union + type: zone + members: ["@member_visegrad_group"] + scope: 'tax' + zone_nato: + code: NATO + name: NATO + type: zone + members: ["@member_poland_nato"] + scope: 'shipping' diff --git a/src/Sylius/Bundle/AddressingBundle/Tests/Repository/ZoneRepositoryTest.php b/src/Sylius/Bundle/AddressingBundle/Tests/Repository/ZoneRepositoryTest.php new file mode 100644 index 0000000000..ff4087a286 --- /dev/null +++ b/src/Sylius/Bundle/AddressingBundle/Tests/Repository/ZoneRepositoryTest.php @@ -0,0 +1,147 @@ +loadFixtures(); + } + + /** @test */ + public function it_finds_a_single_zone_by_address_and_type(): void + { + $address = new Address(); + $address->setCountryCode('PL'); + $address->setProvinceCode('MA'); + + $repository = $this->getRepository(); + $zoneByProvince = $repository->findOneByAddressAndType($address, ZoneInterface::TYPE_PROVINCE); + $zoneByCountry = $repository->findOneByAddressAndType($address, ZoneInterface::TYPE_COUNTRY); + $zoneByZone = $repository->findOneByAddressAndType($address, ZoneInterface::TYPE_ZONE); + + $this->assertNotNull($zoneByProvince); + $this->assertNotNull($zoneByCountry); + $this->assertNull($zoneByZone); + $this->assertSame('POLISH_PROVINCES', $zoneByProvince->getCode()); + $this->assertSame('EU', $zoneByCountry->getCode()); + } + + /** @test */ + public function it_finds_all_zones_for_a_given_address_with_only_country(): void + { + $address = new Address(); + $address->setCountryCode('PL'); + + $repository = $this->getRepository(); + $zones = []; + + foreach ($repository->findByAddress($address) as $zone) { + $zones[$zone->getCode()] = $zone; + } + + $this->assertCount(2, $zones); + $this->assertArrayHasKey('EU', $zones); + $this->assertArrayHasKey('VISEGRAD_GROUP', $zones); + } + + /** @test */ + public function it_finds_all_zones_for_a_given_address_with_restricting_by_scope_if_provided(): void + { + $address = new Address(); + $address->setCountryCode('PL'); + $address->setProvinceCode('MA'); + + $repository = $this->getRepository(); + $zones = []; + + foreach ($repository->findByAddress($address, 'tax') as $zone) { + $zones[$zone->getCode()] = $zone; + } + + $this->assertCount(3, $zones); + $this->assertArrayHasKey('EU', $zones); + $this->assertArrayHasKey('VISEGRAD_GROUP', $zones); + $this->assertArrayHasKey('POLISH_PROVINCES', $zones); + } + + /** @test */ + public function it_finds_all_zones_by_passing_a_zone_member(): void + { + $repository = $this->getRepository(); + $zones = []; + + $visegradGroupZone = $repository->findOneBy(['code' => 'VISEGRAD_GROUP']); + foreach ($repository->findByMembers([$visegradGroupZone]) as $zone) { + $zones[$zone->getCode()] = $zone; + } + + $this->assertCount(2, $zones); + $this->assertArrayHasKey('EU_MIDDLE', $zones); + $this->assertArrayHasKey('NATO', $zones); + } + + /** @test */ + public function it_finds_all_zones_by_passing_a_zone_member_with_restricting_by_scope_if_provided(): void + { + $repository = $this->getRepository(); + $zones = []; + + /** @var ZoneInterface $visegradGroupZone */ + $visegradGroupZone = $repository->findOneBy(['code' => 'VISEGRAD_GROUP']); + + foreach ($repository->findByMembers([$visegradGroupZone], 'tax') as $zone) { + $zones[$zone->getCode()] = $zone; + } + + $this->assertCount(1, $zones); + $this->assertArrayHasKey('EU_MIDDLE', $zones); + $this->assertArrayNotHasKey('NATO', $zones); + } + + private function getRepository(): ZoneRepositoryInterface + { + /** @var ZoneRepositoryInterface $repository */ + $repository = self::getContainer()->get('sylius.repository.zone'); + + return $repository; + } + + private function loadFixtures(): void + { + /** @var LoaderInterface $fixtureLoader */ + $fixtureLoader = self::getContainer()->get('fidry_alice_data_fixtures.loader.doctrine'); + + /** @var EntityManagerInterface $manager */ + $manager = self::getContainer()->get('doctrine.orm.default_entity_manager'); + + (new ORMPurger($manager))->purge(); + + $fixtureLoader->load([ + __DIR__ . '/ZoneRepositoryTest/fixtures.yaml', + ], [], [], PurgeMode::createDeleteMode()); + } +} diff --git a/src/Sylius/Bundle/AddressingBundle/Tests/Repository/ZoneRepositoryTest/fixtures.yaml b/src/Sylius/Bundle/AddressingBundle/Tests/Repository/ZoneRepositoryTest/fixtures.yaml new file mode 100644 index 0000000000..875eb512d6 --- /dev/null +++ b/src/Sylius/Bundle/AddressingBundle/Tests/Repository/ZoneRepositoryTest/fixtures.yaml @@ -0,0 +1,57 @@ +Sylius\Component\Addressing\Model\Country: + country_PL: + code: PL + +Sylius\Component\Addressing\Model\Province: + province_{AL, CA, FL, NY, MA, LD}: + code: + name: + abbreviation: + country: "@country_PL" + +Sylius\Component\Addressing\Model\ZoneMember: + member_eu_country_{NL, BE, PL, DE, CZ, SK, HU}: + code: + member_visegrad_country_{CZ, SK, HU, PL}: + code: + member_province_{AL, CA, FL, NY, MA, LD}: + code: + member_zone_{visegrad}: + code: "VISEGRAD_GROUP" + member_zone_{visegrad}_2: + code: "VISEGRAD_GROUP" + +Sylius\Component\Addressing\Model\Zone: + zone_eu: + code: EU + name: European Union + type: country + members: ["@member_eu_country_NL", "@member_eu_country_BE", "@member_eu_country_PL", "@member_eu_country_DE", "@member_eu_country_CZ", "@member_eu_country_SK", "@member_eu_country_HU"] + scope: 'tax' + zone_eu_middle: + code: EU_MIDDLE + name: European Union Middle + type: zone + members: ["@member_zone_visegrad"] + scope: 'tax' + nato: + code: NATO + name: NATO + type: zone + members: ["@member_zone_visegrad_2"] + scope: 'shipping' + zone_visegrad: + code: VISEGRAD_GROUP + name: Visegrad Group + type: country + members: ["@member_visegrad_country_CZ", "@member_visegrad_country_SK", "@member_visegrad_country_HU", "@member_visegrad_country_PL"] + zone_usa_provinces: + code: USA_PROVINCES + name: USA Provinces + type: province + members: ["@member_province_AL", "@member_province_CA", "@member_province_FL", "@member_province_NY"] + zone_polish_provinces: + code: POLISH_PROVINCES + name: Polish Provinces + type: province + members: ["@member_province_MA", "@member_province_LD"] diff --git a/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ProvinceAddressConstraintValidator.php b/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ProvinceAddressConstraintValidator.php index bd595c690c..fa4996c84a 100644 --- a/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ProvinceAddressConstraintValidator.php +++ b/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ProvinceAddressConstraintValidator.php @@ -27,7 +27,7 @@ class ProvinceAddressConstraintValidator extends ConstraintValidator { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$value instanceof AddressInterface) { throw new \InvalidArgumentException( diff --git a/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/UniqueProvinceCollectionValidator.php b/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/UniqueProvinceCollectionValidator.php index ed73b1245d..caa18df215 100644 --- a/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/UniqueProvinceCollectionValidator.php +++ b/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/UniqueProvinceCollectionValidator.php @@ -21,7 +21,7 @@ use Webmozart\Assert\Assert; final class UniqueProvinceCollectionValidator extends ConstraintValidator { - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var Collection $value */ Assert::allIsInstanceOf($value, ProvinceInterface::class); diff --git a/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ZoneCannotContainItselfValidator.php b/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ZoneCannotContainItselfValidator.php index 9e97374cb1..8519fd1599 100644 --- a/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ZoneCannotContainItselfValidator.php +++ b/src/Sylius/Bundle/AddressingBundle/Validator/Constraints/ZoneCannotContainItselfValidator.php @@ -21,7 +21,7 @@ use Webmozart\Assert\Assert; final class ZoneCannotContainItselfValidator extends ConstraintValidator { - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if ($value === null) { return; diff --git a/src/Sylius/Bundle/AddressingBundle/composer.json b/src/Sylius/Bundle/AddressingBundle/composer.json index b6db5f5248..791f8feee1 100644 --- a/src/Sylius/Bundle/AddressingBundle/composer.json +++ b/src/Sylius/Bundle/AddressingBundle/composer.json @@ -26,12 +26,12 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "stof/doctrine-extensions-bundle": "^1.4", - "sylius/addressing": "^1.12", + "sylius/addressing": "^1.13", "sylius/resource-bundle": "^1.9", - "symfony/framework-bundle": "^5.4 || ^6.0", - "symfony/intl": "^5.4 || ^6.0" + "symfony/framework-bundle": "^5.4.21 || ^6.4", + "symfony/intl": "^5.4.21 || ^6.4" }, "conflict": { "doctrine/orm": ">= 2.16.0", @@ -39,14 +39,17 @@ "twig/twig": "^1.0 || ^3.0" }, "require-dev": { + "doctrine/data-fixtures": "^1.4", "doctrine/orm": "^2.13", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0", - "symfony/validator": "^5.4 || ^6.0" + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4", + "symfony/validator": "^5.4.21 || ^6.4", + "theofidry/alice-data-fixtures": "^1.4" }, "config": { "allow-plugins": { @@ -55,7 +58,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" }, "symfony": { "require": "^5.4" diff --git a/src/Sylius/Bundle/AddressingBundle/phpunit.xml.dist b/src/Sylius/Bundle/AddressingBundle/phpunit.xml.dist index 59fd4c1e9e..3472f2faea 100644 --- a/src/Sylius/Bundle/AddressingBundle/phpunit.xml.dist +++ b/src/Sylius/Bundle/AddressingBundle/phpunit.xml.dist @@ -5,7 +5,16 @@ colors="true" > + + + + + + + + + diff --git a/src/Sylius/Bundle/AddressingBundle/test/app/AppKernel.php b/src/Sylius/Bundle/AddressingBundle/test/app/AppKernel.php index 9653b12db0..366ef171a7 100644 --- a/src/Sylius/Bundle/AddressingBundle/test/app/AppKernel.php +++ b/src/Sylius/Bundle/AddressingBundle/test/app/AppKernel.php @@ -28,6 +28,8 @@ class AppKernel extends Kernel new Sylius\Bundle\AddressingBundle\SyliusAddressingBundle(), new Sylius\Bundle\ResourceBundle\SyliusResourceBundle(), new Symfony\Bundle\TwigBundle\TwigBundle(), + new Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle(), + new Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle(), ]; } diff --git a/src/Sylius/Bundle/AddressingBundle/test/app/config/parameters.yml b/src/Sylius/Bundle/AddressingBundle/test/app/config/parameters.yml index 553212408c..c5beeec62c 100644 --- a/src/Sylius/Bundle/AddressingBundle/test/app/config/parameters.yml +++ b/src/Sylius/Bundle/AddressingBundle/test/app/config/parameters.yml @@ -1,6 +1,6 @@ parameters: database_driver: pdo_sqlite - database_path: "%kernel.project_dir%/app/db.sql" + database_path: "%kernel.project_dir%/var/db.sql" locale: en_US secret: "Three can keep a secret, if two of them are dead." diff --git a/src/Sylius/Bundle/AddressingBundle/test/var/db.sql b/src/Sylius/Bundle/AddressingBundle/test/var/db.sql new file mode 100644 index 0000000000..e0933190b6 Binary files /dev/null and b/src/Sylius/Bundle/AddressingBundle/test/var/db.sql differ diff --git a/src/Sylius/Bundle/AdminBundle/Action/Account/RenderResetPasswordPageAction.php b/src/Sylius/Bundle/AdminBundle/Action/Account/RenderResetPasswordPageAction.php index 77f66d24a4..9792c4b579 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/Account/RenderResetPasswordPageAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/Account/RenderResetPasswordPageAction.php @@ -38,7 +38,14 @@ final class RenderResetPasswordPageAction private string $tokenTtl, ) { if ($this->requestStackOrFlashBag instanceof FlashBagInterface) { - trigger_deprecation('sylius/admin-bundle', '1.12', sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', FlashBagInterface::class, self::class, RequestStack::class)); + trigger_deprecation( + 'sylius/admin-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + FlashBagInterface::class, + self::class, + RequestStack::class, + ); } } diff --git a/src/Sylius/Bundle/AdminBundle/Action/Account/RequestPasswordResetAction.php b/src/Sylius/Bundle/AdminBundle/Action/Account/RequestPasswordResetAction.php index f685419140..53d75621b4 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/Account/RequestPasswordResetAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/Account/RequestPasswordResetAction.php @@ -37,7 +37,14 @@ final class RequestPasswordResetAction private Environment $twig, ) { if ($this->requestStackOrFlashBag instanceof FlashBagInterface) { - trigger_deprecation('sylius/admin-bundle', '1.12', sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', FlashBagInterface::class, self::class, RequestStack::class)); + trigger_deprecation( + 'sylius/admin-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + FlashBagInterface::class, + self::class, + RequestStack::class, + ); } } diff --git a/src/Sylius/Bundle/AdminBundle/Action/Account/ResetPasswordAction.php b/src/Sylius/Bundle/AdminBundle/Action/Account/ResetPasswordAction.php index 3d18efd944..b3ab525f7a 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/Account/ResetPasswordAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/Account/ResetPasswordAction.php @@ -36,7 +36,14 @@ final class ResetPasswordAction private Environment $twig, ) { if ($this->requestStackOrFlashBag instanceof FlashBagInterface) { - trigger_deprecation('sylius/admin-bundle', '1.12', sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', FlashBagInterface::class, self::class, RequestStack::class)); + trigger_deprecation( + 'sylius/admin-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + FlashBagInterface::class, + self::class, + RequestStack::class, + ); } } diff --git a/src/Sylius/Bundle/AdminBundle/Action/ResendOrderConfirmationEmailAction.php b/src/Sylius/Bundle/AdminBundle/Action/ResendOrderConfirmationEmailAction.php index a3cca64783..8d7adfcfbc 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/ResendOrderConfirmationEmailAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/ResendOrderConfirmationEmailAction.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Bundle\AdminBundle\Action; use Sylius\Bundle\AdminBundle\EmailManager\OrderEmailManagerInterface; +use Sylius\Bundle\CoreBundle\MessageDispatcher\ResendOrderConfirmationEmailDispatcherInterface; use Sylius\Bundle\CoreBundle\Provider\FlashBagProvider; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Repository\OrderRepositoryInterface; @@ -31,12 +32,36 @@ final class ResendOrderConfirmationEmailAction { public function __construct( private OrderRepositoryInterface $orderRepository, - private OrderEmailManagerInterface $orderEmailManager, + private OrderEmailManagerInterface|ResendOrderConfirmationEmailDispatcherInterface $orderEmailManager, private CsrfTokenManagerInterface $csrfTokenManager, - private SessionInterface|RequestStack $requestStackOrSession, + private RequestStack|SessionInterface $requestStackOrSession, ) { if ($this->requestStackOrSession instanceof SessionInterface) { - trigger_deprecation('sylius/admin-bundle', '1.12', sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', SessionInterface::class, self::class, RequestStack::class)); + trigger_deprecation( + 'sylius/admin-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + SessionInterface::class, + self::class, + RequestStack::class, + ); + } + + if ($this->orderEmailManager instanceof OrderEmailManagerInterface) { + trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + OrderEmailManagerInterface::class, + self::class, + ResendOrderConfirmationEmailDispatcherInterface::class, + ); + + trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'The argument name $orderEmailManager is deprecated and will be renamed to $resendOrderConfirmationEmailDispatcher in Sylius 2.0.', + ); } } @@ -56,7 +81,7 @@ final class ResendOrderConfirmationEmailAction throw new NotFoundHttpException(sprintf('The order with id %s has not been found', $orderId)); } - $this->orderEmailManager->sendConfirmationEmail($order); + $this->sendConfirmationEmailOrDispatchResendOrderConfirmation($order); FlashBagProvider ::getFlashBag($this->requestStackOrSession) @@ -65,4 +90,13 @@ final class ResendOrderConfirmationEmailAction return new RedirectResponse($request->headers->get('referer')); } + + private function sendConfirmationEmailOrDispatchResendOrderConfirmation(OrderInterface $order): void + { + if ($this->orderEmailManager instanceof OrderEmailManagerInterface) { + $this->orderEmailManager->sendConfirmationEmail($order); + } else { + $this->orderEmailManager->dispatch($order); + } + } } diff --git a/src/Sylius/Bundle/AdminBundle/Action/ResendShipmentConfirmationEmailAction.php b/src/Sylius/Bundle/AdminBundle/Action/ResendShipmentConfirmationEmailAction.php index 397435380c..2a0049b6ed 100644 --- a/src/Sylius/Bundle/AdminBundle/Action/ResendShipmentConfirmationEmailAction.php +++ b/src/Sylius/Bundle/AdminBundle/Action/ResendShipmentConfirmationEmailAction.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Bundle\AdminBundle\Action; use Sylius\Bundle\AdminBundle\EmailManager\ShipmentEmailManagerInterface; +use Sylius\Bundle\CoreBundle\MessageDispatcher\ResendShipmentConfirmationEmailDispatcherInterface; use Sylius\Bundle\CoreBundle\Provider\FlashBagProvider; use Sylius\Component\Core\Model\ShipmentInterface; use Sylius\Component\Core\Repository\ShipmentRepositoryInterface; @@ -31,12 +32,37 @@ final class ResendShipmentConfirmationEmailAction { public function __construct( private ShipmentRepositoryInterface $shipmentRepository, - private ShipmentEmailManagerInterface $shipmentEmailManager, + private ResendShipmentConfirmationEmailDispatcherInterface|ShipmentEmailManagerInterface $shipmentEmailManager, private CsrfTokenManagerInterface $csrfTokenManager, - private SessionInterface|RequestStack $requestStackOrSession, + private RequestStack|SessionInterface $requestStackOrSession, ) { if ($this->requestStackOrSession instanceof SessionInterface) { - trigger_deprecation('sylius/admin-bundle', '1.12', sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', SessionInterface::class, self::class, RequestStack::class)); + trigger_deprecation( + 'sylius/admin-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + SessionInterface::class, + self::class, + RequestStack::class, + ); + } + + if ($this->shipmentEmailManager instanceof ShipmentEmailManagerInterface) { + trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + ShipmentEmailManagerInterface::class, + self::class, + ResendShipmentConfirmationEmailDispatcherInterface::class, + ); + + trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'The argument name $shipmentEmailManager in the constructor of %s is deprecated and will be renamed to $resendShipmentConfirmationDispatcher in Sylius 2.0.', + self::class, + ); } } @@ -56,7 +82,7 @@ final class ResendShipmentConfirmationEmailAction throw new NotFoundHttpException(sprintf('The shipment with id %s has not been found', $shipmentId)); } - $this->shipmentEmailManager->sendConfirmationEmail($shipment); + $this->sendConfirmationEmailOrDispatchResendShipmentConfirmation($shipment); FlashBagProvider ::getFlashBag($this->requestStackOrSession) @@ -65,4 +91,13 @@ final class ResendShipmentConfirmationEmailAction return new RedirectResponse($request->headers->get('referer')); } + + private function sendConfirmationEmailOrDispatchResendShipmentConfirmation(ShipmentInterface $shipment): void + { + if ($this->shipmentEmailManager instanceof ShipmentEmailManagerInterface) { + $this->shipmentEmailManager->sendConfirmationEmail($shipment); + } else { + $this->shipmentEmailManager->dispatch($shipment); + } + } } diff --git a/src/Sylius/Bundle/AdminBundle/Console/Command/ChangeAdminUserPasswordCommand.php b/src/Sylius/Bundle/AdminBundle/Console/Command/ChangeAdminUserPasswordCommand.php new file mode 100644 index 0000000000..36f6f97c8f --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Console/Command/ChangeAdminUserPasswordCommand.php @@ -0,0 +1,78 @@ + $adminUserRepository */ + public function __construct( + private UserRepositoryInterface $adminUserRepository, + private PasswordUpdaterInterface $passwordUpdater, + private QuestionFactoryInterface $questionFactory, + ) { + parent::__construct(); + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if (!$input->isInteractive()) { + $this->io->error('This command must be run interactively.'); + + return Command::FAILURE; + } + + $this->io->title('Change admin user password'); + + $email = $this->io->askQuestion($this->questionFactory->createEmail()); + + /** @var AdminUserInterface|null $adminUser */ + $adminUser = $this->adminUserRepository->findOneByEmail($email); + if ($adminUser === null) { + $this->io->error(sprintf('Admin user with email address %s not found!', $email)); + + return Command::INVALID; + } + + $password = $this->io->askQuestion($this->questionFactory->createWithNotNullValidator('New password', true)); + $adminUser->setPlainPassword($password); + + $this->passwordUpdater->updatePassword($adminUser); + $this->adminUserRepository->add($adminUser); + + $this->io->success('Admin user password has been changed successfully.'); + + return Command::SUCCESS; + } +} diff --git a/src/Sylius/Bundle/AdminBundle/Console/Command/CreateAdminUserCommand.php b/src/Sylius/Bundle/AdminBundle/Console/Command/CreateAdminUserCommand.php new file mode 100644 index 0000000000..0c4c6bce80 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Console/Command/CreateAdminUserCommand.php @@ -0,0 +1,139 @@ +messageBus = $messageBus; + + parent::__construct(); + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + if (!$input->isInteractive()) { + $this->io->error('This command must be run interactively.'); + + return Command::FAILURE; + } + + $this->io->title('Admin user creation'); + + $adminUserData = $this->askAdminUserData(); + + $this->showSummary($adminUserData); + + if (!$this->adminCreationConfirmed()) { + $this->io->error('Admin user creation has been aborted.'); + + return Command::INVALID; + } + + try { + $this->handle(new CreateAdminUser(...array_values($adminUserData))); + } catch (HandlerFailedException $exception) { + $this->io->error( + $exception + ->getNestedExceptionOfClass(CreateAdminUserFailedException::class)[0] + ->getMessage(), + ); + + return Command::FAILURE; + } + + $this->io->success('Admin user has been successfully created.'); + + return Command::SUCCESS; + } + + /** @return array */ + private function askAdminUserData(): array + { + $adminUserData = []; + + $adminUserData['email'] = $this->io->askQuestion($this->questionFactory->createEmail()); + $adminUserData['username'] = $this->io->askQuestion( + $this->questionFactory->createWithNotNullValidator('Username'), + ); + $adminUserData['first_name'] = $this->io->ask('First name'); + $adminUserData['last_name'] = $this->io->ask('Last name'); + $adminUserData['plain_password'] = $this->io->askQuestion( + $this->questionFactory->createWithNotNullValidator('Password', true), + ); + + $localeCodes = Locales::getNames(); + $adminUserData['locale_code'] = $this->io->choice('Locale code', $localeCodes, $this->defaultLocaleCode); + + $adminUserData['enabled'] = $this->io->confirm('Do you want to enable this admin user?', true); + + return $adminUserData; + } + + /** @param array $adminUserData */ + private function showSummary(array $adminUserData): void + { + $this->io->writeln('The following admin user will be created:'); + $this->io->table( + [ + 'Email', 'Username', 'First name', 'Last name', 'Locale code', 'Enabled', + ], + [ + [ + $adminUserData['email'], + $adminUserData['username'], + $adminUserData['first_name'], + $adminUserData['last_name'], + $adminUserData['locale_code'], + $adminUserData['enabled'] ? 'Yes' : 'No', + ], + ], + ); + } + + private function adminCreationConfirmed(): bool + { + return $this->io->confirm('Do you want to save this admin user?'); + } +} diff --git a/src/Sylius/Bundle/AdminBundle/Console/Command/Factory/QuestionFactory.php b/src/Sylius/Bundle/AdminBundle/Console/Command/Factory/QuestionFactory.php new file mode 100644 index 0000000000..98b2162f10 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Console/Command/Factory/QuestionFactory.php @@ -0,0 +1,50 @@ +setValidator(function (?string $email) { + if ($email === null || !filter_var($email, \FILTER_VALIDATE_EMAIL)) { + throw new \InvalidArgumentException('The email address provided is invalid. Please try again.'); + } + + return $email; + }); + $question->setMaxAttempts(3); + + return $question; + } + + public function createWithNotNullValidator(string $askedQuestion, bool $hidden = false): Question + { + $question = new Question($askedQuestion); + $question->setValidator(function (?string $value) { + if ($value === null) { + throw new \InvalidArgumentException('The value cannot be empty.'); + } + + return $value; + }); + $question->setMaxAttempts(3); + $question->setHidden($hidden); + + return $question; + } +} diff --git a/src/Sylius/Bundle/AdminBundle/Console/Command/Factory/QuestionFactoryInterface.php b/src/Sylius/Bundle/AdminBundle/Console/Command/Factory/QuestionFactoryInterface.php new file mode 100644 index 0000000000..33271cfa8b --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Console/Command/Factory/QuestionFactoryInterface.php @@ -0,0 +1,23 @@ +router = $router; } diff --git a/src/Sylius/Bundle/AdminBundle/Controller/NotificationController.php b/src/Sylius/Bundle/AdminBundle/Controller/NotificationController.php index 048b133c80..082936556c 100644 --- a/src/Sylius/Bundle/AdminBundle/Controller/NotificationController.php +++ b/src/Sylius/Bundle/AdminBundle/Controller/NotificationController.php @@ -13,49 +13,85 @@ declare(strict_types=1); namespace Sylius\Bundle\AdminBundle\Controller; -use GuzzleHttp\ClientInterface; +use GuzzleHttp\ClientInterface as DeprecatedClientInterface; use GuzzleHttp\Exception\GuzzleException; -use GuzzleHttp\Psr7\Uri; use Http\Message\MessageFactory; +use Nyholm\Psr7\Stream; +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\StreamFactoryInterface; use Sylius\Bundle\CoreBundle\SyliusCoreBundle; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; final class NotificationController { - private Uri $hubUri; - public function __construct( - private ClientInterface $client, - private MessageFactory $messageFactory, - string $hubUri, + private ClientInterface|DeprecatedClientInterface $client, + private MessageFactory|RequestFactoryInterface $requestFactory, + private string $hubUri, private string $environment, + private ?StreamFactoryInterface $streamFactory = null, ) { - $this->hubUri = new Uri($hubUri); + if (!$client instanceof ClientInterface) { + trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'Using a service that does not implement "%s" as a 1st argument of "%s" constructor is deprecated and will be prohibited in Sylius 2.0.', + ClientInterface::class, + self::class, + ); + } + + if (!$requestFactory instanceof RequestFactoryInterface) { + trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'Using a service that does not implement "%s" as a 2nd argument of "%s" constructor is deprecated and will be prohibited in Sylius 2.0.', + RequestFactoryInterface::class, + self::class, + ); + } + + if (null === $streamFactory) { + trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'Not passing a service that implements "%s" as a 5th argument of "%s" constructor is deprecated and will be prohibited in Sylius 2.0.', + StreamFactoryInterface::class, + self::class, + ); + } } public function getVersionAction(Request $request): JsonResponse { - $content = [ + $content = json_encode([ 'version' => SyliusCoreBundle::VERSION, 'hostname' => $request->getHost(), 'locale' => $request->getLocale(), 'user_agent' => $request->headers->get('User-Agent'), 'environment' => $this->environment, - ]; + ]); - $headers = ['Content-Type' => 'application/json']; - - $hubRequest = $this->messageFactory->createRequest( - Request::METHOD_GET, - $this->hubUri, - $headers, - json_encode($content), - ); + $hubRequest = $this->requestFactory + ->createRequest(Request::METHOD_GET, $this->hubUri) + ->withHeader('Content-Type', 'application/json') + ->withBody( + null === $this->streamFactory + ? Stream::create($content) + : $this->streamFactory->createStream($content), + ) + ; try { - $hubResponse = $this->client->send($hubRequest, ['verify' => false]); - } catch (GuzzleException) { + if ($this->client instanceof DeprecatedClientInterface) { + $hubResponse = $this->client->send($hubRequest, ['verify' => false]); + } else { + $hubResponse = $this->client->sendRequest($hubRequest); + } + } catch (ClientExceptionInterface|GuzzleException) { return new JsonResponse('', JsonResponse::HTTP_NO_CONTENT); } diff --git a/src/Sylius/Bundle/AdminBundle/DependencyInjection/Compiler/BackwardsCompatibility/ReplaceEmailManagersPass.php b/src/Sylius/Bundle/AdminBundle/DependencyInjection/Compiler/BackwardsCompatibility/ReplaceEmailManagersPass.php new file mode 100644 index 0000000000..9d439e6bf0 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/DependencyInjection/Compiler/BackwardsCompatibility/ReplaceEmailManagersPass.php @@ -0,0 +1,89 @@ +getDefinitions() as $definition) { + if ($definition->getDecoratedService() === null) { + continue; + } + + $decoratedServiceId = $definition->getDecoratedService()[0]; + + if ($decoratedServiceId === OrderEmailManagerInterface::class) { + $this->replaceArgument( + $container, + ResendOrderConfirmationEmailAction::class, + ResendOrderConfirmationEmailAction::class, + 1, + OrderEmailManagerInterface::class, + ); + + continue; + } + + if ($decoratedServiceId === 'sylius.email_manager.shipment') { + $this->replaceArgument( + $container, + 'sylius.listener.shipment_ship', + ShipmentShipListener::class, + 0, + 'sylius.email_manager.shipment', + ); + + continue; + } + + if ($decoratedServiceId === ShipmentEmailManagerInterface::class) { + $this->replaceArgument( + $container, + ResendShipmentConfirmationEmailAction::class, + ResendShipmentConfirmationEmailAction::class, + 1, + ShipmentEmailManagerInterface::class, + ); + } + } + } + + public function replaceArgument( + ContainerBuilder $container, + string $serviceId, + string $serviceClass, + int $argumentIndex, + string $argumentId, + ): void { + if (!$container->hasDefinition($serviceId)) { + return; + } + + $definition = $container->findDefinition($serviceId); + if ($definition->getClass() === $serviceClass) { + $definition->setArgument($argumentIndex, new Reference($argumentId)); + } + } +} diff --git a/src/Sylius/Bundle/AdminBundle/EmailManager/OrderEmailManager.php b/src/Sylius/Bundle/AdminBundle/EmailManager/OrderEmailManager.php index 213e25eb48..2402b6e37f 100644 --- a/src/Sylius/Bundle/AdminBundle/EmailManager/OrderEmailManager.php +++ b/src/Sylius/Bundle/AdminBundle/EmailManager/OrderEmailManager.php @@ -18,6 +18,15 @@ use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Mailer\Sender\SenderInterface; use Webmozart\Assert\Assert; +trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'The "%s" class is deprecated, use "%s" instead.', + OrderEmailManager::class, + \Sylius\Bundle\CoreBundle\Mailer\OrderEmailManager::class, +); + +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see \Sylius\Bundle\CoreBundle\Mailer\OrderEmailManager} instead. */ final class OrderEmailManager implements OrderEmailManagerInterface { public function __construct(private SenderInterface $emailSender) diff --git a/src/Sylius/Bundle/AdminBundle/EmailManager/OrderEmailManagerInterface.php b/src/Sylius/Bundle/AdminBundle/EmailManager/OrderEmailManagerInterface.php index 0639b507ef..4ffabddc29 100644 --- a/src/Sylius/Bundle/AdminBundle/EmailManager/OrderEmailManagerInterface.php +++ b/src/Sylius/Bundle/AdminBundle/EmailManager/OrderEmailManagerInterface.php @@ -15,6 +15,15 @@ namespace Sylius\Bundle\AdminBundle\EmailManager; use Sylius\Component\Core\Model\OrderInterface; +trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'The "%s" interface is deprecated, use "%s" instead.', + OrderEmailManagerInterface::class, + \Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface::class, +); + +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see \Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface} instead. */ interface OrderEmailManagerInterface { public function sendConfirmationEmail(OrderInterface $order): void; diff --git a/src/Sylius/Bundle/AdminBundle/EmailManager/ShipmentEmailManager.php b/src/Sylius/Bundle/AdminBundle/EmailManager/ShipmentEmailManager.php index f0a951a95f..90463039ee 100644 --- a/src/Sylius/Bundle/AdminBundle/EmailManager/ShipmentEmailManager.php +++ b/src/Sylius/Bundle/AdminBundle/EmailManager/ShipmentEmailManager.php @@ -19,6 +19,15 @@ use Sylius\Component\Core\Model\ShipmentInterface; use Sylius\Component\Mailer\Sender\SenderInterface; use Webmozart\Assert\Assert; +trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'The "%s" class is deprecated, use "%s" instead.', + ShipmentEmailManager::class, + \Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManager::class, +); + +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see \Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManager} instead. */ final class ShipmentEmailManager implements ShipmentEmailManagerInterface { public function __construct(private SenderInterface $emailSender) diff --git a/src/Sylius/Bundle/AdminBundle/EmailManager/ShipmentEmailManagerInterface.php b/src/Sylius/Bundle/AdminBundle/EmailManager/ShipmentEmailManagerInterface.php index 051c78ea44..668086ad5c 100644 --- a/src/Sylius/Bundle/AdminBundle/EmailManager/ShipmentEmailManagerInterface.php +++ b/src/Sylius/Bundle/AdminBundle/EmailManager/ShipmentEmailManagerInterface.php @@ -15,6 +15,15 @@ namespace Sylius\Bundle\AdminBundle\EmailManager; use Sylius\Component\Core\Model\ShipmentInterface; +trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'The "%s" interface is deprecated, use "%s" instead.', + ShipmentEmailManagerInterface::class, + \Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface::class, +); + +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see \Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface} instead. */ interface ShipmentEmailManagerInterface { public function sendConfirmationEmail(ShipmentInterface $shipment): void; diff --git a/src/Sylius/Bundle/AdminBundle/Event/OrderShowMenuBuilderEvent.php b/src/Sylius/Bundle/AdminBundle/Event/OrderShowMenuBuilderEvent.php index a8ccb5c22d..368d199925 100644 --- a/src/Sylius/Bundle/AdminBundle/Event/OrderShowMenuBuilderEvent.php +++ b/src/Sylius/Bundle/AdminBundle/Event/OrderShowMenuBuilderEvent.php @@ -15,7 +15,8 @@ namespace Sylius\Bundle\AdminBundle\Event; use Knp\Menu\FactoryInterface; use Knp\Menu\ItemInterface; -use SM\StateMachine\StateMachineInterface; +use SM\StateMachine\StateMachineInterface as WinzouStateMachineInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\UiBundle\Menu\Event\MenuBuilderEvent; use Sylius\Component\Core\Model\OrderInterface; @@ -25,9 +26,21 @@ class OrderShowMenuBuilderEvent extends MenuBuilderEvent FactoryInterface $factory, ItemInterface $menu, private OrderInterface $order, - private StateMachineInterface $stateMachine, + private StateMachineInterface|WinzouStateMachineInterface $stateMachine, ) { parent::__construct($factory, $menu); + + if ($this->stateMachine instanceof WinzouStateMachineInterface) { + trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the fourth argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + WinzouStateMachineInterface::class, + StateMachineInterface::class, + ), + ); + } } public function getOrder(): OrderInterface @@ -35,7 +48,7 @@ class OrderShowMenuBuilderEvent extends MenuBuilderEvent return $this->order; } - public function getStateMachine(): StateMachineInterface + public function getStateMachine(): StateMachineInterface|WinzouStateMachineInterface { return $this->stateMachine; } diff --git a/src/Sylius/Bundle/AdminBundle/EventListener/LocaleListener.php b/src/Sylius/Bundle/AdminBundle/EventListener/LocaleListener.php new file mode 100644 index 0000000000..9b966e7824 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/EventListener/LocaleListener.php @@ -0,0 +1,38 @@ +getSubject(); + + if (!$this->localeUsageChecker->isUsed($locale->getCode())) { + return; + } + + $event->stop('sylius.locale.delete.is_used', errorCode: Response::HTTP_UNPROCESSABLE_ENTITY); + } +} diff --git a/src/Sylius/Bundle/AdminBundle/EventListener/ResourceDeleteSubscriber.php b/src/Sylius/Bundle/AdminBundle/EventListener/ResourceDeleteSubscriber.php index 6b40b6710b..8e3605ad77 100644 --- a/src/Sylius/Bundle/AdminBundle/EventListener/ResourceDeleteSubscriber.php +++ b/src/Sylius/Bundle/AdminBundle/EventListener/ResourceDeleteSubscriber.php @@ -29,10 +29,17 @@ final class ResourceDeleteSubscriber implements EventSubscriberInterface { public function __construct( private UrlGeneratorInterface $router, - private SessionInterface|RequestStack $requestStackOrSession, + private RequestStack|SessionInterface $requestStackOrSession, ) { if ($this->requestStackOrSession instanceof SessionInterface) { - trigger_deprecation('sylius/admin-bundle', '1.12', sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', SessionInterface::class, self::class, RequestStack::class)); + trigger_deprecation( + 'sylius/admin-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + SessionInterface::class, + self::class, + RequestStack::class, + ); } } diff --git a/src/Sylius/Bundle/AdminBundle/EventListener/ShipmentShipListener.php b/src/Sylius/Bundle/AdminBundle/EventListener/ShipmentShipListener.php index f50cbd7a57..7a77677b76 100644 --- a/src/Sylius/Bundle/AdminBundle/EventListener/ShipmentShipListener.php +++ b/src/Sylius/Bundle/AdminBundle/EventListener/ShipmentShipListener.php @@ -13,15 +13,26 @@ declare(strict_types=1); namespace Sylius\Bundle\AdminBundle\EventListener; -use Sylius\Bundle\AdminBundle\EmailManager\ShipmentEmailManagerInterface; +use Sylius\Bundle\AdminBundle\EmailManager\ShipmentEmailManagerInterface as DeprecatedShipmentEmailManagerInterface; +use Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface; use Sylius\Component\Core\Model\ShipmentInterface; use Symfony\Component\EventDispatcher\GenericEvent; use Webmozart\Assert\Assert; final class ShipmentShipListener { - public function __construct(private ShipmentEmailManagerInterface $shipmentEmailManager) + public function __construct(private DeprecatedShipmentEmailManagerInterface|ShipmentEmailManagerInterface $shipmentEmailManager) { + if ($this->shipmentEmailManager instanceof DeprecatedShipmentEmailManagerInterface) { + trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be prohibited in Sylius 2.0. Pass an instance of %s instead.', + DeprecatedShipmentEmailManagerInterface::class, + self::class, + ShipmentEmailManagerInterface::class, + ); + } } public function sendConfirmationEmail(GenericEvent $event): void diff --git a/src/Sylius/Bundle/AdminBundle/Exception/CreateAdminUserFailedException.php b/src/Sylius/Bundle/AdminBundle/Exception/CreateAdminUserFailedException.php new file mode 100644 index 0000000000..d9fae154e9 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Exception/CreateAdminUserFailedException.php @@ -0,0 +1,18 @@ +stateMachineFactory instanceof StateMachineFactoryInterface) { + trigger_deprecation( + 'sylius/admin-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the third argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + StateMachineFactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function createMenu(array $options): ItemInterface @@ -50,11 +63,11 @@ final class OrderShowMenuBuilder ]) ->setAttribute('type', 'link') ->setLabel('sylius.ui.history') - ->setLabelAttribute('icon', 'history') - ; + ->setLabelAttribute('icon', 'history'); - $stateMachine = $this->stateMachineFactory->get($order, OrderTransitions::GRAPH); - if ($stateMachine->can(OrderTransitions::TRANSITION_CANCEL)) { + $stateMachine = $this->getStateMachine(); + + if ($stateMachine->can($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CANCEL)) { $menu ->addChild('cancel', [ 'route' => 'sylius_admin_order_cancel', @@ -67,8 +80,7 @@ final class OrderShowMenuBuilder ->setAttribute('confirmation', true) ->setLabel('sylius.ui.cancel') ->setLabelAttribute('icon', 'ban') - ->setLabelAttribute('color', 'yellow') - ; + ->setLabelAttribute('color', 'yellow'); } $this->eventDispatcher->dispatch( @@ -78,4 +90,13 @@ final class OrderShowMenuBuilder return $menu; } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } } diff --git a/src/Sylius/Bundle/AdminBundle/Message/CreateAdminUser.php b/src/Sylius/Bundle/AdminBundle/Message/CreateAdminUser.php new file mode 100644 index 0000000000..667c3c5543 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Message/CreateAdminUser.php @@ -0,0 +1,63 @@ +email; + } + + public function getUsername(): string + { + return $this->username; + } + + public function getPlainPassword(): string + { + return $this->plainPassword; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function getLocaleCode(): string + { + return $this->localeCode; + } + + public function isEnabled(): bool + { + return $this->enabled; + } +} diff --git a/src/Sylius/Bundle/AdminBundle/MessageHandler/CreateAdminUserHandler.php b/src/Sylius/Bundle/AdminBundle/MessageHandler/CreateAdminUserHandler.php new file mode 100644 index 0000000000..dd5d56b128 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/MessageHandler/CreateAdminUserHandler.php @@ -0,0 +1,80 @@ + $adminUserRepository + * @param FactoryInterface $adminUserFactory + * @param array $validationGroups + */ + public function __construct( + private UserRepositoryInterface $adminUserRepository, + private FactoryInterface $adminUserFactory, + private CanonicalizerInterface $canonicalizer, + private ValidatorInterface $validator, + private array $validationGroups, + ) { + } + + public function __invoke(CreateAdminUser $command): void + { + $adminUser = $this->setUpAdminUser($command); + + $constraintViolationList = $this->validator->validate($adminUser, null, $this->validationGroups); + + if ($constraintViolationList->count()) { + $violationMessages = $this->getViolationMessages($constraintViolationList); + + throw new CreateAdminUserFailedException(implode(\PHP_EOL, [...$violationMessages])); + } + + $this->adminUserRepository->add($adminUser); + } + + private function setUpAdminUser(CreateAdminUser $command): AdminUserInterface + { + /** @var AdminUserInterface $adminUser */ + $adminUser = $this->adminUserFactory->createNew(); + + $adminUser->setEmail($this->canonicalizer->canonicalize($command->getEmail())); + $adminUser->setUsername($command->getUsername()); + $adminUser->setPlainPassword($command->getPlainPassword()); + $adminUser->setFirstName($command->getFirstName()); + $adminUser->setLastName($command->getLastName()); + $adminUser->setLocaleCode($command->getLocaleCode()); + $adminUser->setEnabled($command->isEnabled()); + + return $adminUser; + } + + /** @return iterable */ + private function getViolationMessages(ConstraintViolationListInterface $constraintViolationList): iterable + { + foreach ($constraintViolationList as $violation) { + yield $violation->getMessage(); + } + } +} diff --git a/src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProvider.php b/src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProvider.php index 5de937e566..1bed627098 100644 --- a/src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProvider.php +++ b/src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProvider.php @@ -14,7 +14,6 @@ declare(strict_types=1); namespace Sylius\Bundle\AdminBundle\Provider; use Sylius\Bundle\MoneyBundle\Formatter\MoneyFormatterInterface; -use Sylius\Component\Core\Dashboard\DashboardStatistics; use Sylius\Component\Core\Dashboard\DashboardStatisticsProviderInterface; use Sylius\Component\Core\Dashboard\Interval; use Sylius\Component\Core\Dashboard\SalesDataProviderInterface; @@ -31,7 +30,6 @@ class StatisticsDataProvider implements StatisticsDataProviderInterface public function getRawData(ChannelInterface $channel, \DateTimeInterface $startDate, \DateTimeInterface $endDate, string $interval): array { - /** @var DashboardStatistics $statistics */ $statistics = $this->statisticsProvider->getStatisticsForChannelInPeriod($channel, $startDate, $endDate); $salesSummary = $this->salesDataProvider->getSalesSummary( diff --git a/src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProviderInterface.php b/src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProviderInterface.php index ca90a0ff2e..eda47e58cf 100644 --- a/src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProviderInterface.php +++ b/src/Sylius/Bundle/AdminBundle/Provider/StatisticsDataProviderInterface.php @@ -17,5 +17,6 @@ use Sylius\Component\Core\Model\ChannelInterface; interface StatisticsDataProviderInterface { + /** @return array> */ public function getRawData(ChannelInterface $channel, \DateTimeInterface $startDate, \DateTimeInterface $endDate, string $interval): array; } diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/app/config.yml index 939e61c91d..9c1dd7975d 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/app/config.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/app/config.yml @@ -7,6 +7,7 @@ imports: - { resource: "@SyliusAdminBundle/Resources/config/grids/admin_user.yml" } - { resource: "@SyliusAdminBundle/Resources/config/grids/catalog_promotion.yaml" } - { resource: "@SyliusAdminBundle/Resources/config/grids/channel.yml" } + - { resource: "@SyliusAdminBundle/Resources/config/grids/channel_pricing_log_entry.yml" } - { resource: "@SyliusAdminBundle/Resources/config/grids/country.yml" } - { resource: "@SyliusAdminBundle/Resources/config/grids/currency.yml" } - { resource: "@SyliusAdminBundle/Resources/config/grids/customer.yml" } diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/app/events.yaml b/src/Sylius/Bundle/AdminBundle/Resources/config/app/events.yaml index d1a7983418..4f45dcf2fc 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/app/events.yaml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/app/events.yaml @@ -608,24 +608,24 @@ sylius_ui: customer: template: "@SyliusAdmin/Order/Show/_customer.html.twig" priority: 50 - before_addresses_legacy: - template: "@SyliusUi/Block/_legacySonataEvent.html.twig" - priority: 45 - context: - event: sylius.admin.order.show.before_addresses - addresses: - template: "@SyliusAdmin/Order/Show/_addresses.html.twig" - priority: 40 before_payments_legacy: template: "@SyliusUi/Block/_legacySonataEvent.html.twig" - priority: 35 + priority: 45 context: event: sylius.admin.order.show.before_payments payments: template: "@SyliusAdmin/Order/Show/_payments.html.twig" - priority: 30 + priority: 40 shipments: template: "@SyliusAdmin/Order/Show/_shipments.html.twig" + priority: 35 + before_addresses_legacy: + template: "@SyliusUi/Block/_legacySonataEvent.html.twig" + priority: 30 + context: + event: sylius.admin.order.show.before_addresses + addresses: + template: "@SyliusAdmin/Order/Show/_addresses.html.twig" priority: 20 resend_email: template: "@SyliusAdmin/Order/Show/_resendEmail.html.twig" @@ -986,7 +986,7 @@ sylius_ui: priority: 20 content: template: "@SyliusAdmin/Crud/Index/_content.html.twig" - priority: 10 + priority: 10 sylius.admin.catalog_promotion.product_variant.index.header.content: blocks: @@ -1119,6 +1119,9 @@ sylius_ui: look_and_feel: template: '@SyliusAdmin/Channel/Form/_lookAndFeel.html.twig' priority: 10 + price_history: + template: '@SyliusAdmin/Channel/Form/_priceHistoryConfig.html.twig' + priority: 0 sylius.admin.customer.form: blocks: diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/channel_pricing_log_entry.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/channel_pricing_log_entry.yml new file mode 100644 index 0000000000..ca01e23cd2 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/channel_pricing_log_entry.yml @@ -0,0 +1,30 @@ +sylius_grid: + grids: + sylius_admin_channel_pricing_log_entry: + driver: + name: doctrine/orm + options: + class: '%sylius.model.channel_pricing_log_entry.class%' + repository: + method: createByChannelPricingIdListQueryBuilder + arguments: [$channelPricingId] + fields: + price: + type: twig + label: sylius.ui.price + path: . + options: + template: '@SyliusAdmin/ChannelPricingLogEntry/Grid/Field/price.html.twig' + originalPrice: + type: twig + label: sylius.ui.original_price + path: . + options: + template: '@SyliusAdmin/ChannelPricingLogEntry/Grid/Field/originalPrice.html.twig' + loggedAt: + type: datetime + label: sylius.ui.logged_at + filters: + loggedAt: + type: date + label: sylius.ui.logged_at diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/customer.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/customer.yml index 036f7072be..f9b50007d6 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/customer.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/customer.yml @@ -44,6 +44,18 @@ sylius_grid: label: sylius.ui.search options: fields: [email, firstName, lastName] + group: + type: resource_autocomplete + label: sylius.ui.customer_groups + form_options: + resource: sylius.customer_group + choice_name: name + choice_value: code + multiple: true + remote_path: sylius_admin_ajax_customer_groups_by_phrase + load_edit_path: sylius_admin_ajax_customer_group_by_code + options: + fields: [group.code] actions: main: create: diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/customer_order.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/customer_order.yml index b7b4dadd83..52405e8a6b 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/customer_order.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/customer_order.yml @@ -5,7 +5,7 @@ sylius_grid: driver: options: repository: - method: createByCustomerIdQueryBuilder + method: createByCustomerIdCriteriaAwareQueryBuilder arguments: customerId: $id fields: diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/locale.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/locale.yml index ebc029af1c..393ab2bb74 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/locale.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/locale.yml @@ -30,3 +30,5 @@ sylius_grid: item: update: type: update + delete: + type: delete diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/order.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/order.yml index e000828f09..426c6b5e6d 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/order.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/order.yml @@ -6,7 +6,9 @@ sylius_grid: options: class: "%sylius.model.order.class%" repository: - method: createSearchListQueryBuilder + method: createCriteriaAwareSearchListQueryBuilder + arguments: + criteria: $criteria sorting: number: desc fields: diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/product_review.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/product_review.yml index 21ee1144b5..1a311bcb04 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/product_review.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/product_review.yml @@ -41,6 +41,14 @@ sylius_grid: title: type: string label: sylius.ui.title + status: + type: select + label: sylius.ui.status + form_options: + choices: + sylius.ui.new: new + sylius.ui.accepted: accepted + sylius.ui.rejected: rejected actions: item: update: diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/promotion.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/promotion.yml index 0e9782964e..839202b661 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/grids/promotion.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/grids/promotion.yml @@ -51,6 +51,12 @@ sylius_grid: label: sylius.ui.coupon options: fields: [coupons.code] + archival: + type: exists + label: sylius.ui.archival + options: + field: archivedAt + default_value: false actions: main: create: @@ -85,6 +91,8 @@ sylius_grid: type: update delete: type: delete + archive: + type: archive bulk: delete: type: delete diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax.yml index 8166434e6f..283040fb69 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax.yml @@ -18,6 +18,10 @@ sylius_admin_ajax_product_variant: resource: "@SyliusAdminBundle/Resources/config/routing/ajax/product_variant.yml" prefix: /product-variants +sylius_admin_ajax_customer_group: + resource: "@SyliusAdminBundle/Resources/config/routing/ajax/customer_group.yml" + prefix: /customer-groups + sylius_admin_ajax_product_option: resource: "@SyliusAdminBundle/Resources/config/routing/ajax/product_option.yml" prefix: /product-options diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/customer_group.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/customer_group.yml new file mode 100644 index 0000000000..a810f8c8bb --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/customer_group.yml @@ -0,0 +1,27 @@ +sylius_admin_ajax_customer_groups_by_phrase: + path: /search-phrase + methods: [GET] + defaults: + _controller: sylius.controller.customer_group::indexAction + _format: json + _sylius: + serialization_groups: [Autocomplete] + permission: true + repository: + method: findByPhrase + arguments: + phrase: $phrase + limit: "!!int %sylius.ajax.customer_group.autocomplete_limit%" + +sylius_admin_ajax_customer_group_by_code: + path: /code + methods: [GET] + defaults: + _controller: sylius.controller.customer_group::indexAction + _format: json + _sylius: + serialization_groups: [Autocomplete] + permission: true + repository: + method: findBy + arguments: [code: $code] diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/taxon.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/taxon.yml index 0dc13c77a5..7639ef523c 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/taxon.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/ajax/taxon.yml @@ -60,6 +60,7 @@ sylius_admin_ajax_generate_taxon_slug: _controller: sylius.controller.taxon_slug::generateAction _format: json +# This route is deprecated since Sylius 1.13.0 and will be removed in 2.0.0. sylius_admin_ajax_taxon_move: path: /{id}/move methods: [PUT] diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/locale.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/locale.yml index 5c8979c2f5..726ef516bc 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/locale.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/locale.yml @@ -15,3 +15,13 @@ sylius_admin_locale: index: icon: translate type: sylius.resource + +sylius_admin_locale_delete: + path: /locales/{id} + methods: [DELETE] + defaults: + _controller: sylius.controller.locale::deleteAction + _sylius: + section: admin + redirect: referer + permission: true diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/product.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/product.yml index 8932192746..969e42d0e2 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/product.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/product.yml @@ -39,6 +39,9 @@ sylius_admin_product_update: section: admin permission: true redirect: referer + repository: + method: findOneByIdHydrated + arguments: $id template: "@SyliusAdmin/Crud/update.html.twig" vars: subheader: sylius.ui.manage_your_product_catalog @@ -106,3 +109,21 @@ sylius_admin_product_show: section: admin permission: true template: "@SyliusAdmin/Product/show.html.twig" + +sylius_admin_channel_pricing_log_entry_index: + path: products/{productId}/variants/{variantId}/channel-pricing/{channelPricingId}/channel-pricing-log-entries + methods: [GET] + defaults: + _controller: sylius.controller.channel_pricing_log_entry::indexAction + _sylius: + section: admin + permission: true + template: '@SyliusAdmin/Crud/index.html.twig' + grid: sylius_admin_channel_pricing_log_entry + vars: + icon: book + templates: + breadcrumb: "@SyliusAdmin/ChannelPricingLogEntry/Index/_breadcrumb.html.twig" + header: sylius.ui.channel_pricing_history + subheader: sylius.ui.show_history_for_channel_pricing + product_variant: "expr:service('sylius.repository.product_variant').find($variantId)" diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/promotion.yml b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/promotion.yml index 9e9c757342..8489d7bdbd 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/routing/promotion.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/routing/promotion.yml @@ -19,3 +19,18 @@ sylius_admin_promotion: form: "@SyliusAdmin/Promotion/_form.html.twig" toolbar: "@SyliusAdmin/Promotion/_toolbar.html.twig" type: sylius.resource + +sylius_admin_promotion_archive: + path: /promotions/{id}/archive + methods: [PATCH] + defaults: + _controller: sylius.controller.promotion::updateAction + _sylius: + section: admin + permission: true + template: '@SyliusUi/Grid/Action/archive.html.twig' + form: + type: Sylius\Bundle\ResourceBundle\Form\Type\ArchivableType + redirect: + route: sylius_admin_promotion_index + parameters: {} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/services.xml b/src/Sylius/Bundle/AdminBundle/Resources/config/services.xml index 6ca3cac889..b52eeea818 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/services.xml @@ -25,11 +25,44 @@ 25 25 10 + 25 + + sylius + sylius_user_create + + + + %sylius_locale.locale% + + + + + + + + + + + + + + + + + %sylius.message.admin_user_create.validation_groups% + + + + + + + + @@ -62,7 +95,9 @@ - - + + + The "%service_id%" service is deprecated since 1.13 and will be removed in 2.0. Use "Psr\Http\Message\RequestFactoryInterface" instead. + diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/services/controller.xml b/src/Sylius/Bundle/AdminBundle/Resources/config/services/controller.xml index e5ae6e690b..b5b0b67099 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/services/controller.xml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/services/controller.xml @@ -44,14 +44,14 @@ - + - + @@ -73,7 +73,6 @@ - @@ -90,9 +89,10 @@ - + %sylius.admin.notification.uri% %kernel.environment% + diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/services/email.xml b/src/Sylius/Bundle/AdminBundle/Resources/config/services/email.xml index 81ed9df412..1f77dcef08 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/services/email.xml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/services/email.xml @@ -17,11 +17,19 @@ + The "%service_id%" service is deprecated since 1.13 and will be removed in 2.0. Use "sylius.mailer.order_email_manager" instead. + The "%service_id%" service is deprecated since 1.13 and will be removed in 2.0. Use "sylius.mailer.shipment_email_manager.admin" instead. + + + The "%alias_id%" service is deprecated since 1.13 and will be removed in 2.0. Use "sylius.mailer.shipment_email_manager.admin" instead. + + + + - diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/services/listener.xml b/src/Sylius/Bundle/AdminBundle/Resources/config/services/listener.xml index ab3fdd5a3b..52561395e5 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/services/listener.xml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/services/listener.xml @@ -16,10 +16,16 @@ - + + + + + + + diff --git a/src/Sylius/Bundle/AdminBundle/Resources/config/services/menu.xml b/src/Sylius/Bundle/AdminBundle/Resources/config/services/menu.xml index 65fe8a65e6..ce9c1a4d23 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/config/services/menu.xml +++ b/src/Sylius/Bundle/AdminBundle/Resources/config/services/menu.xml @@ -36,7 +36,7 @@ - + diff --git a/src/Sylius/Bundle/AdminBundle/Resources/translations/flashes.en.yml b/src/Sylius/Bundle/AdminBundle/Resources/translations/flashes.en.yml index f647ba0f8b..992508b368 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/translations/flashes.en.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/translations/flashes.en.yml @@ -13,3 +13,7 @@ sylius: shipment_confirmation_resent: 'Shipment confirmation has been successfully resent to the customer.' product_variant: cannot_generate_variants: 'Cannot generate variants for a product without options values.' + locale: + delete: + is_used: 'Cannot delete the locale, as it is used by at least one translation.' + success: 'Locale has been successfully deleted.' diff --git a/src/Sylius/Bundle/AdminBundle/Resources/translations/flashes.fr.yml b/src/Sylius/Bundle/AdminBundle/Resources/translations/flashes.fr.yml index 3990fa8f7f..b5f4e9cf77 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/translations/flashes.fr.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/translations/flashes.fr.yml @@ -13,3 +13,7 @@ sylius: shipment_confirmation_resent: 'La confirmation d''expédition a été renvoyée avec succès au client.' product_variant: cannot_generate_variants: 'Impossible de générer des variantes pour un produit sans options.' + locale: + delete: + is_used: 'Impossible de supprimer la langue, car elle est utilisée par au moins une traduction.' + success: 'La langue a été supprimée avec succès.' diff --git a/src/Sylius/Bundle/AdminBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/AdminBundle/Resources/translations/messages.en.yml index 38b57d9b2c..85bb1d3aee 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/translations/messages.en.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/translations/messages.en.yml @@ -8,6 +8,9 @@ sylius: your_order_with_number: 'Your order with number' form: admin: + channel: + lowest_price_for_discounted_products_visible: 'Show the lowest price of discounted products prior to the current discount?' + period_for_which_the_lowest_price_is_calculated: 'Period for which the lowest price is calculated' reset_password: password: label: New password @@ -62,19 +65,23 @@ sylius: dates_details: This operation is time-consuming! Please consider a 2-10 minutes delay starting from the specified dates. The delay depends on the size of the catalog. channel: account_verification_required_details: By disabling this option, it will be possible to create accounts and access them without email verification and thus potentially access order data placed as a guest. + channel_pricing_history: 'Channel pricing history' channel_pricings: Channel pricings - product: - product_not_active_in_channel: The product is not yet activated in this channel. - original_price_details: Original price - this is the price of the product variant It is displayed as crossed-out in the catalog. It is used as the base for current price calculations. If this value is not defined, Catalog Promotion logic will copy value from Price to Original Price. - price_details: Price - this is the current price of the product variant displayed in the catalog. It can be modified explicitly by i.e. catalog promotions. - minimum_price_details: Minimum price - this is the pricing threshold below which the current price cannot be discounted by neither catalog nor cart promotions. Use this to guard the profitability of your sales. gateway: no_sca_support_notice: The chosen payment gateway does not support SCA. pay_pal_express_checkout_deprecation_notice: > PayPal Express Checkout is deprecated. Please, use new PayPal Commerce Platform integration. + lowest_price_before_discount: 'Lowest price before discount' + minimum_price_details: Minimum price - this is the pricing threshold below which the current price cannot be discounted by neither catalog nor cart promotions. Use this to guard the profitability of your sales. + original_price_details: Original price - this is the price of the product variant It is displayed as crossed-out in the catalog. It is used as the base for current price calculations. If this value is not defined, Catalog Promotion logic will copy value from Price to Original Price. + price_details: Price - this is the current price of the product variant displayed in the catalog. It can be modified explicitly by i.e. catalog promotions. + price_history: Price history + product: + product_not_active_in_channel: The product is not yet activated in this channel. sales_summary: Sales summary shipment_for_order: Shipment for order + show_history_for_channel_pricing: 'Show history for channel pricing' statistics: day: Day lifetime: Lifetime diff --git a/src/Sylius/Bundle/AdminBundle/Resources/translations/messages.fr.yml b/src/Sylius/Bundle/AdminBundle/Resources/translations/messages.fr.yml index 21c8108dce..364418c915 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/translations/messages.fr.yml +++ b/src/Sylius/Bundle/AdminBundle/Resources/translations/messages.fr.yml @@ -11,6 +11,9 @@ sylius: your_order_with_number: 'Votre commande numéro' form: admin: + channel: + lowest_price_for_discounted_products_visible: 'Afficher le prix le plus bas des produits faisant l''objet d''une remise avant la remise en cours ?' + period_for_which_the_lowest_price_is_calculated: 'Période pour laquelle le prix le plus bas est calculé' reset_password: password: label: Nouveau mot de passe @@ -65,18 +68,22 @@ sylius: dates_details: Cette opération prend du temps ! Veuillez tenir compte d'un délai de 2 à 10 minutes à partir des dates spécifiées. Le délai dépend de la taille du catalogue. channel: account_verification_required_details: En désactivant cette option, il sera possible de créer des comptes et d'y accéder sans vérification de l'adresse électronique et donc d'accéder potentiellement aux données des commandes passées en tant qu'invité. + channel_pricing_history: 'Historique des prix des canaux' channel_pricings: Prix du canal - product: - product_not_active_in_channel: Le produit n'est pas encore activé pour ce canal. - original_price_details: Prix original (aussi appelé prix barré) - il s'agit du prix de la variante de produit, il est affiché barré dans le catalogue. Il est utilisé comme base pour les calculs de prix actuels. Si cette valeur n'est pas définie, la logique de promotion du catalogue (Catalog Promotion) copiera la valeur du prix dans le prix d'origine. - price_details: 'Prix - il s''agit du prix actuel de la variante de produit affichée dans le catalogue. Il peut être modifié par des mécanismes internes, ex: les promotions du catalogue.' - minimum_price_details: Prix minimum - il s'agit du seuil de prix en dessous duquel le prix actuel ne peut être remisé par une quelconque promotion. Utilisez ceci pour garder la rentabilité de vos ventes. gateway: no_sca_support_notice: L'interface de paiement choisie ne prend pas en charge l'authentification forte du client. pay_pal_express_checkout_deprecation_notice: > PayPal Express Checkout est obsolète. Veuillez utiliser la nouvelle intégration de PayPal Commerce Platform. + lowest_price_before_discount: 'Prix le plus bas avant remise' + minimum_price_details: Prix minimum - il s'agit du seuil de prix en dessous duquel le prix actuel ne peut être remisé par une quelconque promotion. Utilisez ceci pour garder la rentabilité de vos ventes. + original_price_details: Prix original (aussi appelé prix barré) - il s'agit du prix de la variante de produit, il est affiché barré dans le catalogue. Il est utilisé comme base pour les calculs de prix actuels. Si cette valeur n'est pas définie, la logique de promotion du catalogue (Catalog Promotion) copiera la valeur du prix dans le prix d'origine. + price_details: 'Prix - il s''agit du prix actuel de la variante de produit affichée dans le catalogue. Il peut être modifié par des mécanismes internes, ex: les promotions du catalogue.' + price_history: Historique des prix + product: + product_not_active_in_channel: Le produit n'est pas encore activé pour ce canal. sales_summary: Récapitulatif des ventes shipment_for_order: Expédition pour la commande + show_history_for_channel_pricing: 'Afficher l''historique des prix des canaux' statistics: day: Jour lifetime: Durée de vie diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Channel/Form/_priceHistoryConfig.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Channel/Form/_priceHistoryConfig.html.twig new file mode 100644 index 0000000000..a950b2caf0 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Channel/Form/_priceHistoryConfig.html.twig @@ -0,0 +1,7 @@ +{% form_theme form '@SyliusAdmin/Form/theme.html.twig' %} + +
+ {{ form_row(form.channelPriceHistoryConfig.lowestPriceForDiscountedProductsVisible) }} + {{ form_row(form.channelPriceHistoryConfig.lowestPriceForDiscountedProductsCheckingPeriod) }} + {{ form_row(form.channelPriceHistoryConfig.taxonsExcludedFromShowingLowestPrice, {'remote_url': path('sylius_admin_ajax_taxon_by_name_phrase'), 'load_edit_url': path('sylius_admin_ajax_taxon_by_code')}) }} +
diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/ChannelPricingLogEntry/Grid/Field/originalPrice.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/ChannelPricingLogEntry/Grid/Field/originalPrice.html.twig new file mode 100644 index 0000000000..f30b178739 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/ChannelPricingLogEntry/Grid/Field/originalPrice.html.twig @@ -0,0 +1,10 @@ +{% import "@SyliusAdmin/Common/Macro/money.html.twig" as money %} + +{% set currencies = sylius_channels_currencies() %} +{% set channel_code = data.channelPricing.channelCode %} + +{% if data.originalPrice %} + {{ money.format(data.originalPrice, currencies[channel_code]) }} +{% else %} + - +{% endif %} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/ChannelPricingLogEntry/Grid/Field/price.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/ChannelPricingLogEntry/Grid/Field/price.html.twig new file mode 100644 index 0000000000..1e1ae6592c --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/ChannelPricingLogEntry/Grid/Field/price.html.twig @@ -0,0 +1,6 @@ +{% import "@SyliusAdmin/Common/Macro/money.html.twig" as money %} + +{% set currencies = sylius_channels_currencies() %} +{% set channel_code = data.channelPricing.channelCode %} + +{{ money.format(data.price, currencies[channel_code]) }} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/ChannelPricingLogEntry/Index/_breadcrumb.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/ChannelPricingLogEntry/Index/_breadcrumb.html.twig new file mode 100644 index 0000000000..4a0e27e724 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/ChannelPricingLogEntry/Index/_breadcrumb.html.twig @@ -0,0 +1,13 @@ +{% import '@SyliusAdmin/Macro/breadcrumb.html.twig' as breadcrumb %} + +{% set product_variant = configuration.vars.product_variant %} +{% set product = configuration.vars.product_variant.product %} + +{{ breadcrumb.crumble([ + { label: 'sylius.ui.administration'|trans, url: path('sylius_admin_dashboard') }, + { label: 'sylius.ui.products'|trans, url: path('sylius_admin_product_index') }, + { label: product.name|default(product.code), url: path('sylius_admin_product_show', {'id': product.id }) }, + { label: 'sylius.ui.variants'|trans, url: path('sylius_admin_product_variant_index', {'productId': product.id }) }, + { label: product_variant.name|default(product_variant.code), url: path('sylius_admin_product_variant_update', {'id': product_variant.id, 'productId': product.id }) }, + { label: 'sylius.ui.price_history'|trans }, +]) }} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Email/orderConfirmation.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Email/orderConfirmation.html.twig index ade862fdf7..7c98785ab9 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/views/Email/orderConfirmation.html.twig +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Email/orderConfirmation.html.twig @@ -1,4 +1,4 @@ -{% deprecated 'The "@SyliusAdmin/Email/orderConfirmation.html.twig" template is deprecated since Sylius 1.8 and will be removed in Sylius 2.0, use "@SyliusCore/Email/layout.html.twig" instead.' %} +{% deprecated 'The "@SyliusAdmin/Email/orderConfirmation.html.twig" template is deprecated since Sylius 1.8 and will be removed in Sylius 2.0, use "@SyliusCore/Email/orderConfirmation.html.twig" instead.' %} {% extends '@SyliusAdmin/Email/layout.html.twig' %} {% block subject %} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Form/theme.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Form/theme.html.twig index 6d3955c959..89947c4516 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/views/Form/theme.html.twig +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Form/theme.html.twig @@ -46,3 +46,12 @@ {% endfor %} {% endblock %} + +{% block _sylius_channel_channelPriceHistoryConfig_lowestPriceForDiscountedProductsCheckingPeriod_widget %} +
+ {{ form_widget(form) }} +
+ {{ 'sylius.ui.days'|trans }} +
+
+{% endblock %} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Order/Show/_resendEmail.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Order/Show/_resendEmail.html.twig index f63e013d15..a8f1811a03 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/views/Order/Show/_resendEmail.html.twig +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Order/Show/_resendEmail.html.twig @@ -1,6 +1,8 @@ -
- {% set path = path('sylius_admin_order_resend_confirmation_email', {'id': order.id, '_csrf_token': csrf_token(order.id)}) %} - - {{ 'sylius.ui.resend_the_order_confirmation_email'|trans }} - -
+{% if order.state in sylius_order_states_that_allows_to_resend_order_confirmation_email %} +
+ {% set path = path('sylius_admin_order_resend_confirmation_email', {'id': order.id, '_csrf_token': csrf_token(order.id)}) %} + + {{ 'sylius.ui.resend_the_order_confirmation_email'|trans }} + +
+{% endif %} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_pricing.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_pricing.html.twig index a3f533cee9..948bd60219 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_pricing.html.twig +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_pricing.html.twig @@ -8,7 +8,9 @@ {{ 'sylius.ui.channels'|trans }} {{ 'sylius.ui.price'|trans }} {{ 'sylius.ui.original_price'|trans }} + {{ 'sylius.ui.lowest_price_before_discount'|trans }} {{ 'sylius.ui.discounted_by'|trans }} + {{ 'sylius.ui.history'|trans }} @@ -19,12 +21,19 @@ {{ channelPricing.channelCode|sylius_channel_name }} {{ money.format(channelPricing.price, product.channels.first.baseCurrency) }} - {% if channelPricing.originalPrice != null %} - {{ money.format(channelPricing.originalPrice, product.channels.first.baseCurrency) }} - {% else %} - - - {% endif %} + {{ channelPricing.originalPrice ? money.format(channelPricing.originalPrice, product.channels.first.baseCurrency) : '-' }} + {{ channelPricing.lowestPriceBeforeDiscount ? money.format(channelPricing.lowestPriceBeforeDiscount, product.channels.first.baseCurrency) : '-' }} {% include '@SyliusAdmin/Product/Show/_appliedPromotions.html.twig' %} + + + + {{ 'sylius.ui.show'|trans }} + + {% endif %} {% endfor %} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_simpleProduct.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_simpleProduct.html.twig index 85bd8975e9..d9ab73da7d 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_simpleProduct.html.twig +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_simpleProduct.html.twig @@ -1,12 +1,12 @@ {% include '@SyliusAdmin/Product/Show/_header.html.twig' %}
-
+
{{ sylius_template_event('sylius.admin.simple_product.show.details', _context) }} {{ sylius_template_event('sylius.admin.simple_product.show.taxonomy', _context) }}
-
+
{{ sylius_template_event('sylius.admin.simple_product.show.pricing', _context) }} {{ sylius_template_event('sylius.admin.simple_product.show.shipping', _context) }} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_variantContentPricing.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_variantContentPricing.html.twig index e49792d68a..16184f88ee 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_variantContentPricing.html.twig +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Product/Show/_variantContentPricing.html.twig @@ -7,11 +7,13 @@
- + + + {% set currencies = sylius_channels_currencies() %} {% for channelPricing in variant.channelPricings %} @@ -19,14 +21,20 @@ - {% set channelCode = channelPricing.channelCode %} - - {% if channelPricing.originalPrice != null %} - - {% else %} - - {% endif %} + + + {% include '@SyliusAdmin/Product/Show/_appliedPromotions.html.twig' %} + {% endfor %} diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Promotion/Show/_translations.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Promotion/Show/_translations.html.twig new file mode 100644 index 0000000000..1d807ef960 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Promotion/Show/_translations.html.twig @@ -0,0 +1,21 @@ + + +
+ {% for translation in promotion.translations %} +
+ + + {{ translation.locale|sylius_locale_name }} +
+
+
{{ 'sylius.ui.channels'|trans }} {{ 'sylius.ui.price'|trans }} {{ 'sylius.ui.original_price'|trans }}{{ 'sylius.ui.lowest_price_before_discount'|trans }} {{ 'sylius.ui.discounted_by'|trans }}{{ 'sylius.ui.history'|trans }}
{{ channelPricing.channelCode|sylius_channel_name }} {{ money.format(channelPricing.price, currencies[channelCode]) }}{{ money.format(channelPricing.originalPrice, currencies[channelCode]) }}-{{ money.format(channelPricing.price, product.channels.first.baseCurrency) }}{{ channelPricing.originalPrice ? money.format(channelPricing.originalPrice, product.channels.first.baseCurrency) : '-' }}{{ channelPricing.lowestPriceBeforeDiscount ? money.format(channelPricing.lowestPriceBeforeDiscount, product.channels.first.baseCurrency) : '-' }} + + + {{ 'sylius.ui.show'|trans }} + +
+ + + + + + +
{{ 'sylius.ui.label'|trans }}{{ translation.label }}
+
+ {% endfor %} +
diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Promotion/_form.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Promotion/_form.html.twig index 6dd836cf72..6180553ad0 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/views/Promotion/_form.html.twig +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Promotion/_form.html.twig @@ -1,3 +1,5 @@ +{% from '@SyliusAdmin/Macro/translationForm.html.twig' import translationForm %} +
{{ form_errors(form) }} @@ -34,6 +36,9 @@
+
+ {{ translationForm(form.translations) }} +

{{ 'sylius.ui.configuration'|trans }}

diff --git a/src/Sylius/Bundle/AdminBundle/Resources/views/Taxon/_slugField.html.twig b/src/Sylius/Bundle/AdminBundle/Resources/views/Taxon/_slugField.html.twig index 5a02e9b02a..b541cea194 100644 --- a/src/Sylius/Bundle/AdminBundle/Resources/views/Taxon/_slugField.html.twig +++ b/src/Sylius/Bundle/AdminBundle/Resources/views/Taxon/_slugField.html.twig @@ -4,7 +4,7 @@ {{ form_widget(slugField, {'attr': {'data-url': path('sylius_admin_ajax_generate_taxon_slug'), 'data-parent': app.request.attributes.get('id')}}) }} {% else %}
- {{ form_widget(slugField, {'attr': {'readonly': 'readonly', 'data-url': path('sylius_admin_ajax_generate_taxon_slug')}}) }} + {{ form_widget(slugField, {'attr': {'readonly': 'readonly', 'data-url': path('sylius_admin_ajax_generate_taxon_slug'), 'data-parent': resource.parent ? resource.parent.id : null}}) }} diff --git a/src/Sylius/Bundle/AdminBundle/SyliusAdminBundle.php b/src/Sylius/Bundle/AdminBundle/SyliusAdminBundle.php index 1ad56984b8..45f7f5254a 100644 --- a/src/Sylius/Bundle/AdminBundle/SyliusAdminBundle.php +++ b/src/Sylius/Bundle/AdminBundle/SyliusAdminBundle.php @@ -13,8 +13,16 @@ declare(strict_types=1); namespace Sylius\Bundle\AdminBundle; +use Sylius\Bundle\AdminBundle\DependencyInjection\Compiler\BackwardsCompatibility\ReplaceEmailManagersPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; final class SyliusAdminBundle extends Bundle { + public function build(ContainerBuilder $container): void + { + parent::build($container); + + $container->addCompilerPass(new ReplaceEmailManagersPass()); + } } diff --git a/src/Sylius/Bundle/AdminBundle/Tests/Console/Command/ChangeAdminUserPasswordCommandTest.php b/src/Sylius/Bundle/AdminBundle/Tests/Console/Command/ChangeAdminUserPasswordCommandTest.php new file mode 100644 index 0000000000..37d15f7e2f --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Tests/Console/Command/ChangeAdminUserPasswordCommandTest.php @@ -0,0 +1,151 @@ +userRepository = $this->createMock(UserRepositoryInterface::class); + $this->passwordUpdater = $this->createMock(PasswordUpdaterInterface::class); + $this->questionFactory = $this->createMock(QuestionFactoryInterface::class); + + $this->command = new CommandTester( + new ChangeAdminUserPasswordCommand($this->userRepository, $this->passwordUpdater, $this->questionFactory), + ); + } + + /** @test */ + public function it_does_not_execute_in_non_interactive_mode(): void + { + $this->command->execute([], ['interactive' => false]); + + self::assertSame(Command::FAILURE, $this->command->getStatusCode()); + } + + /** @test */ + public function it_does_not_change_password_when_admin_user_is_not_found(): void + { + $this + ->questionFactory + ->expects($this->once()) + ->method('createEmail') + ->willReturn($this->createQuestionMock('Email')); + + $this + ->userRepository + ->expects($this->once()) + ->method('findOneByEmail') + ->willReturn(null); + + $this + ->command + ->setInputs([ + 'email' => self::EMAIL, + ]); + + $this->command->execute([]); + + self::assertSame(Command::INVALID, $this->command->getStatusCode()); + } + + /** @test */ + public function it_changes_password_for_existing_admin_user(): void + { + $adminUser = $this->createMock(AdminUser::class); + $adminUser + ->expects($this->once()) + ->method('setPlainPassword') + ->with(self::PASSWORD); + + $this + ->questionFactory + ->expects($this->once()) + ->method('createEmail') + ->willReturn($this->createQuestionMock('Email')); + + $this + ->questionFactory + ->expects($this->once()) + ->method('createWithNotNullValidator') + ->with('New password', true) + ->willReturn($this->createQuestionMock('New password')); + + $this + ->userRepository + ->expects($this->once()) + ->method('findOneByEmail') + ->willReturn($adminUser); + $this + ->userRepository + ->expects($this->once()) + ->method('add') + ->with($adminUser); + + $this + ->passwordUpdater + ->expects($this->once()) + ->method('updatePassword') + ->with($adminUser); + + $this + ->command + ->setInputs([ + 'email' => self::EMAIL, + 'password' => self::PASSWORD, + ]); + + $this->command->execute([]); + + self::assertSame(Command::SUCCESS, $this->command->getStatusCode()); + } + + private function createQuestionMock(string $askedQuestion): MockObject + { + $question = $this->createMock(Question::class); + $question + ->method('isTrimmable') + ->willReturn(true); + $question + ->method('getQuestion') + ->willReturn($askedQuestion); + + return $question; + } +} diff --git a/src/Sylius/Bundle/AdminBundle/Tests/Console/Command/CreateAdminUserCommandTest.php b/src/Sylius/Bundle/AdminBundle/Tests/Console/Command/CreateAdminUserCommandTest.php new file mode 100644 index 0000000000..5f2de1eb84 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Tests/Console/Command/CreateAdminUserCommandTest.php @@ -0,0 +1,218 @@ +messageBus = $this->createMock(MessageBusInterface::class); + $this->questionFactory = $this->createMock(QuestionFactoryInterface::class); + + $this->command = new CommandTester( + new CreateAdminUserCommand($this->messageBus, self::LOCALE_CODE, $this->questionFactory), + ); + } + + /** @test */ + public function it_creates_an_admin_user_if_accepted_in_the_summary(): void + { + $this + ->questionFactory + ->expects($this->once()) + ->method('createEmail') + ->willReturn($this->createQuestionMock('Email')); + + $this + ->questionFactory + ->expects($this->exactly(2)) + ->method('createWithNotNullValidator') + ->willReturnOnConsecutiveCalls( + $this->createQuestionMock('Username'), + $this->createQuestionMock('New password'), + ); + + $this->command->setInputs($this->getDefaultCommandInputsSetup()); + + $message = new CreateAdminUser(...array_values($this->getDefaultAdminUserDataSetup())); + + $this->messageBus->expects($this->once()) + ->method('dispatch') + ->with($message) + ->willReturn(new Envelope($message, [new HandledStamp(self::anything(), 'handler')])) + ; + + $this->command->execute([]); + + $this->command->assertCommandIsSuccessful(); + self::assertStringContainsString('Admin user has been successfully created.', $this->command->getDisplay()); + } + + /** @test */ + public function it_does_not_create_an_admin_user_if_declined_in_the_summary(): void + { + $this->command->setInputs([ + 'email' => self::EMAIL, + 'username' => self::USERNAME, + 'firstname' => 'Sylius', + 'lastname' => 'Admin', + 'password' => 'sylius', + 'localeCode' => self::LOCALE_CODE, + 'admin_user_enabled' => self::YES, + 'creation_confirmation' => self::NO, + ]); + + $this + ->questionFactory + ->expects($this->once()) + ->method('createEmail') + ->willReturn($this->createQuestionMock('Email')); + + $this + ->questionFactory + ->expects($this->exactly(2)) + ->method('createWithNotNullValidator') + ->willReturnOnConsecutiveCalls( + $this->createQuestionMock('Username'), + $this->createQuestionMock('New password'), + ); + + $this->messageBus->expects($this->never())->method('dispatch'); + + self::assertSame(Command::INVALID, $this->command->execute([])); + self::assertStringContainsString('Admin user creation has been aborted.', $this->command->getDisplay()); + } + + /** @test */ + public function it_does_not_create_an_admin_user_if_dispatched_command_returns_failure(): void + { + $adminUserData = $this->getDefaultAdminUserDataSetup(); + + $this->command->setInputs($this->getDefaultCommandInputsSetup()); + + $message = new CreateAdminUser(...array_values($adminUserData)); + + $this + ->questionFactory + ->expects($this->once()) + ->method('createEmail') + ->willReturn($this->createQuestionMock('Email')); + + $this + ->questionFactory + ->expects($this->exactly(2)) + ->method('createWithNotNullValidator') + ->willReturnOnConsecutiveCalls( + $this->createQuestionMock('Username'), + $this->createQuestionMock('New password'), + ); + + $this->messageBus->expects($this->once()) + ->method('dispatch') + ->with($message) + ->willThrowException(new HandlerFailedException( + new Envelope($message), + [new CreateAdminUserFailedException('Some validation error')], + )) + ; + + $this->command->execute([]); + + self::assertSame(Command::FAILURE, $this->command->getStatusCode()); + } + + /** @test */ + public function it_does_not_create_an_admin_user_if_command_is_not_interactive(): void + { + self::assertSame(Command::FAILURE, $this->command->execute([], ['interactive' => false])); + } + + private function getDefaultCommandInputsSetup(): array + { + return [ + 'email' => self::EMAIL, + 'username' => self::USERNAME, + 'first_name' => self::FIRST_NAME, + 'last_name' => self::LAST_NAME, + 'password' => self::PASSWORD, + 'locale_code' => self::LOCALE_CODE, + 'admin_user_enabled' => self::YES, + 'creation_confirmation' => self::YES, + ]; + } + + private function getDefaultAdminUserDataSetup(): array + { + return [ + 'email' => self::EMAIL, + 'username' => self::USERNAME, + 'first_name' => self::FIRST_NAME, + 'last_name' => self::LAST_NAME, + 'password' => self::PASSWORD, + 'locale_code' => self::LOCALE_CODE, + 'admin_user_enabled' => true, + ]; + } + + private function createQuestionMock(string $askedQuestion): MockObject + { + $question = $this->createMock(Question::class); + $question + ->method('isTrimmable') + ->willReturn(true); + $question + ->method('getQuestion') + ->willReturn($askedQuestion); + + return $question; + } +} diff --git a/src/Sylius/Bundle/AdminBundle/Tests/Console/Command/Factory/QuestionFactoryTest.php b/src/Sylius/Bundle/AdminBundle/Tests/Console/Command/Factory/QuestionFactoryTest.php new file mode 100644 index 0000000000..48c7b10e81 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Tests/Console/Command/Factory/QuestionFactoryTest.php @@ -0,0 +1,78 @@ +createEmail(); + + $this->assertSame('Email', $question->getQuestion()); + $this->assertSame(3, $question->getMaxAttempts()); + $this->assertEquals('test@example.com', $question->getValidator()('test@example.com')); + } + + /** @test */ + public function it_creates_email_question_with_invalid_email(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The email address provided is invalid. Please try again.'); + + $questionFactory = new QuestionFactory(); + $emailQuestion = $questionFactory->createEmail(); + + $emailQuestion->getValidator()('invalid-email'); + } + + /** @test */ + public function it_creates_email_question_with_null_email(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The email address provided is invalid. Please try again.'); + + $questionFactory = new QuestionFactory(); + $emailQuestion = $questionFactory->createEmail(); + + $emailQuestion->getValidator()(null); + } + + /** @test */ + public function it_creates_question_with_not_null_validator(): void + { + $questionFactory = new QuestionFactory(); + $question = $questionFactory->createWithNotNullValidator('Question'); + + $this->assertSame('Question', $question->getQuestion()); + $this->assertSame(3, $question->getMaxAttempts()); + $this->assertEquals('test', $question->getValidator()('test')); + } + + /** @test */ + public function it_creates_question_with_not_null_validator_with_null_value(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The value cannot be empty.'); + + $questionFactory = new QuestionFactory(); + $question = $questionFactory->createWithNotNullValidator('Question'); + + $question->getValidator()(null); + } +} diff --git a/src/Sylius/Bundle/AdminBundle/Tests/Controller/NotificationControllerTest.php b/src/Sylius/Bundle/AdminBundle/Tests/Controller/NotificationControllerTest.php index ccf2c5b01c..2532723ed6 100644 --- a/src/Sylius/Bundle/AdminBundle/Tests/Controller/NotificationControllerTest.php +++ b/src/Sylius/Bundle/AdminBundle/Tests/Controller/NotificationControllerTest.php @@ -13,15 +13,17 @@ declare(strict_types=1); namespace Sylius\Bundle\AdminBundle\Tests\Controller; -use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\ConnectException; use Http\Message\MessageFactory; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ProphecyInterface; +use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamFactoryInterface; use Psr\Http\Message\StreamInterface; use Sylius\Bundle\AdminBundle\Controller\NotificationController; use Symfony\Component\HttpFoundation\JsonResponse; @@ -29,24 +31,37 @@ use Symfony\Component\HttpFoundation\Request; final class NotificationControllerTest extends TestCase { + use ProphecyTrait; + private ObjectProphecy $client; + private ObjectProphecy $legacyClient; + + private ObjectProphecy $requestFactory; + private ObjectProphecy $messageFactory; + private ObjectProphecy $streamFactory; + private NotificationController $controller; + private NotificationController $legacyController; + private static string $hubUri = 'www.doesnotexist.test.com'; - /** - * @test - */ + /** @test */ public function it_returns_an_empty_json_response_upon_client_exception(): void { - $this->messageFactory->createRequest(Argument::any(), Argument::cetera()) - ->willReturn($this->prophesize(RequestInterface::class)->reveal()) - ; + $requestInterface = $this->prophesize(RequestInterface::class); + $streamInterface = $this->prophesize(StreamInterface::class); - $this->client->send(Argument::cetera())->willThrow(ConnectException::class); + $this->streamFactory->createStream(Argument::cetera())->willReturn($streamInterface); + + $this->requestFactory->createRequest(Argument::cetera())->willReturn($requestInterface); + $requestInterface->withHeader('Content-Type', 'application/json')->willReturn($requestInterface); + $requestInterface->withBody($streamInterface)->willReturn($requestInterface); + + $this->client->sendRequest(Argument::cetera())->willThrow(ConnectException::class); $emptyResponse = $this->controller->getVersionAction(new Request()); @@ -56,14 +71,38 @@ final class NotificationControllerTest extends TestCase /** * @test + * + * @legacy This test will be removed in Sylius 2.0. */ + public function it_returns_an_empty_json_response_upon_client_exception_deprecated(): void + { + $requestInterface = $this->prophesize(RequestInterface::class); + + $this->messageFactory->createRequest(Argument::cetera())->willReturn($requestInterface); + $requestInterface->withHeader('Content-Type', 'application/json')->willReturn($requestInterface); + $requestInterface->withBody(Argument::any())->willReturn($requestInterface); + + $this->legacyClient->send(Argument::cetera())->willThrow(ConnectException::class); + + $emptyResponse = $this->legacyController->getVersionAction(new Request()); + + $this->assertEquals(JsonResponse::HTTP_NO_CONTENT, $emptyResponse->getStatusCode()); + $this->assertEquals('""', $emptyResponse->getContent()); + } + + /** @test */ public function it_returns_json_response_from_client_on_success(): void { $content = json_encode(['version' => '9001']); - $this->messageFactory->createRequest(Argument::any(), Argument::cetera()) - ->willReturn($this->prophesize(RequestInterface::class)->reveal()) - ; + $requestInterface = $this->prophesize(RequestInterface::class); + $streamInterface = $this->prophesize(StreamInterface::class); + + $this->streamFactory->createStream(Argument::cetera())->willReturn($streamInterface); + + $this->requestFactory->createRequest(Argument::cetera())->willReturn($requestInterface); + $requestInterface->withHeader('Content-Type', 'application/json')->willReturn($requestInterface); + $requestInterface->withBody($streamInterface)->willReturn($requestInterface); /** @var ProphecyInterface|StreamInterface $stream */ $stream = $this->prophesize(StreamInterface::class); @@ -73,7 +112,7 @@ final class NotificationControllerTest extends TestCase $externalResponse = $this->prophesize(ResponseInterface::class); $externalResponse->getBody()->willReturn($stream->reveal()); - $this->client->send(Argument::cetera())->willReturn($externalResponse->reveal()); + $this->client->sendRequest(Argument::cetera())->willReturn($externalResponse->reveal()); $response = $this->controller->getVersionAction(new Request()); @@ -81,13 +120,55 @@ final class NotificationControllerTest extends TestCase $this->assertEquals($content, $response->getContent()); } + /** + * @test + * + * @legacy This test will be removed in Sylius 2.0. + */ + public function it_returns_json_response_from_client_on_success_deprecated(): void + { + $content = json_encode(['version' => '9001']); + + $requestInterface = $this->prophesize(RequestInterface::class); + + $this->messageFactory->createRequest(Argument::cetera())->willReturn($requestInterface); + $requestInterface->withHeader('Content-Type', 'application/json')->willReturn($requestInterface); + $requestInterface->withBody(Argument::any())->willReturn($requestInterface); + + /** @var ProphecyInterface|StreamInterface $stream */ + $stream = $this->prophesize(StreamInterface::class); + $stream->getContents()->willReturn($content); + + /** @var ProphecyInterface|ResponseInterface $externalResponse */ + $externalResponse = $this->prophesize(ResponseInterface::class); + $externalResponse->getBody()->willReturn($stream->reveal()); + + $this->legacyClient->send(Argument::cetera())->willReturn($externalResponse->reveal()); + + $response = $this->legacyController->getVersionAction(new Request()); + + $this->assertEquals(JsonResponse::HTTP_OK, $response->getStatusCode()); + $this->assertEquals($content, $response->getContent()); + } + protected function setUp(): void { - $this->client = $this->prophesize(ClientInterface::class); + $this->client = $this->prophesize(\Psr\Http\Client\ClientInterface::class); + $this->legacyClient = $this->prophesize(\GuzzleHttp\ClientInterface::class); + $this->requestFactory = $this->prophesize(RequestFactoryInterface::class); $this->messageFactory = $this->prophesize(MessageFactory::class); + $this->streamFactory = $this->prophesize(StreamFactoryInterface::class); $this->controller = new NotificationController( $this->client->reveal(), + $this->requestFactory->reveal(), + self::$hubUri, + 'environment', + $this->streamFactory->reveal(), + ); + + $this->legacyController = new NotificationController( + $this->legacyClient->reveal(), $this->messageFactory->reveal(), self::$hubUri, 'environment', diff --git a/src/Sylius/Bundle/AdminBundle/Tests/DependencyInjection/Compiler/BackwardsCompatibility/ReplaceEmailManagersPassTest.php b/src/Sylius/Bundle/AdminBundle/Tests/DependencyInjection/Compiler/BackwardsCompatibility/ReplaceEmailManagersPassTest.php new file mode 100644 index 0000000000..754e89fb12 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Tests/DependencyInjection/Compiler/BackwardsCompatibility/ReplaceEmailManagersPassTest.php @@ -0,0 +1,138 @@ +setDefinition('sylius.email_manager.shipment', new Definition()); + $this->setDefinition('sylius.email_manager.shipment.decorated', (new Definition())->setDecoratedService('sylius.email_manager.shipment')); + $this->setDefinition('sylius.mailer.shipment_email_manager.admin', new Definition()); + $this->setDefinition('sylius.listener.shipment_ship', new Definition(ShipmentShipListener::class, [new Reference('sylius.mailer.shipment_email_manager.admin')])); + + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + 'sylius.listener.shipment_ship', + 0, + 'sylius.email_manager.shipment', + ); + } + + /** @test */ + public function it_does_nothing_if_shipment_email_manager_service_is_not_decorated(): void + { + $this->setDefinition('sylius.email_manager.shipment', new Definition()); + $this->setDefinition('sylius.mailer.shipment_email_manager.admin', new Definition()); + $this->setDefinition('sylius.listener.shipment_ship', new Definition(ShipmentShipListener::class, [new Reference('sylius.mailer.shipment_email_manager.admin')])); + + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + 'sylius.listener.shipment_ship', + 0, + 'sylius.mailer.shipment_email_manager.admin', + ); + } + + /** @test */ + public function it_replaces_decorated_shipment_email_manager_service_of_resend_shipment_confirmation_action_arguments(): void + { + $this->setDefinition(ShipmentEmailManagerInterface::class, new Definition()); + $this->setDefinition('sylius.email_manager.shipment.decorated', (new Definition())->setDecoratedService(ShipmentEmailManagerInterface::class)); + $this->setDefinition(ResendShipmentConfirmationEmailDispatcher::class, new Definition()); + $this->setDefinition('sylius.repository.shipment', new Definition()); + $this->setDefinition(ResendShipmentConfirmationEmailAction::class, new Definition(ResendShipmentConfirmationEmailAction::class, [new Reference('sylius.repository.shipment'), new Reference(ResendShipmentConfirmationEmailDispatcher::class)])); + + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + ResendShipmentConfirmationEmailAction::class, + 1, + ShipmentEmailManagerInterface::class, + ); + } + + /** @test */ + public function it_does_nothing_if_shipment_email_manager_interface_service_is_not_decorated(): void + { + $this->setDefinition(ShipmentEmailManagerInterface::class, new Definition()); + $this->setDefinition(ResendShipmentConfirmationEmailDispatcher::class, new Definition()); + $this->setDefinition('sylius.repository.shipment', new Definition()); + $this->setDefinition(ResendShipmentConfirmationEmailAction::class, new Definition(ResendShipmentConfirmationEmailAction::class, [new Reference('sylius.repository.shipment'), new Reference(ResendShipmentConfirmationEmailDispatcher::class)])); + + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + ResendShipmentConfirmationEmailAction::class, + 1, + ResendShipmentConfirmationEmailDispatcher::class, + ); + } + + /** @test */ + public function it_replaces_decorated_order_email_manager_service_of_resend_order_confirmation_action_arguments(): void + { + $this->setDefinition(OrderEmailManagerInterface::class, new Definition()); + $this->setDefinition('sylius.email_manager.order.decorated', (new Definition())->setDecoratedService(OrderEmailManagerInterface::class)); + $this->setDefinition(ResendOrderConfirmationEmailDispatcher::class, new Definition()); + $this->setDefinition('sylius.repository.order', new Definition()); + $this->setDefinition(ResendOrderConfirmationEmailAction::class, new Definition(ResendOrderConfirmationEmailAction::class, [new Reference('sylius.repository.order'), new Reference(ResendOrderConfirmationEmailDispatcher::class)])); + + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + ResendOrderConfirmationEmailAction::class, + 1, + OrderEmailManagerInterface::class, + ); + } + + /** @test */ + public function it_does_nothing_if_order_email_manager_interface_service_is_not_decorated(): void + { + $this->setDefinition(OrderEmailManagerInterface::class, new Definition()); + $this->setDefinition(ResendOrderConfirmationEmailDispatcher::class, new Definition()); + $this->setDefinition('sylius.repository.order', new Definition()); + $this->setDefinition(ResendOrderConfirmationEmailAction::class, new Definition(ResendOrderConfirmationEmailAction::class, [new Reference('sylius.repository.order'), new Reference(ResendOrderConfirmationEmailDispatcher::class)])); + + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + ResendOrderConfirmationEmailAction::class, + 1, + ResendOrderConfirmationEmailDispatcher::class, + ); + } + + protected function registerCompilerPass(ContainerBuilder $container): void + { + $container->addCompilerPass(new ReplaceEmailManagersPass()); + } +} diff --git a/src/Sylius/Bundle/AdminBundle/Tests/MessageHandler/Admin/CreateAdminUserHandlerTest.php b/src/Sylius/Bundle/AdminBundle/Tests/MessageHandler/Admin/CreateAdminUserHandlerTest.php new file mode 100644 index 0000000000..d94f4a7053 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/Tests/MessageHandler/Admin/CreateAdminUserHandlerTest.php @@ -0,0 +1,154 @@ +adminUserFactory = $this->createMock(FactoryInterface::class); + $this->adminUser = $this->createMock(AdminUserInterface::class); + $this->canonicalizer = $this->createMock(CanonicalizerInterface::class); + $this->validator = $this->createMock(ValidatorInterface::class); + $this->adminUserRepository = $this->createMock(UserRepositoryInterface::class); + } + + /** @test */ + public function it_creates_an_admin_user_if_there_is_no_violations(): void + { + $constraintViolationList = new ConstraintViolationList([]); + + $this->arrangePartially($constraintViolationList); + + $this->adminUserRepository->expects($this->once())->method('add'); + + $createAdminUserHandler = $this->createAdminUserHandler(); + + $createAdminUserHandler($this->createAdminUserMessage()); + } + + /** @test */ + public function it_throws_an_exception_if_violates_any_constraints(): void + { + $firstConstraintViolation = new ConstraintViolation('first_violation_error', '', [], '', '', ''); + $secondConstraintViolation = new ConstraintViolation('second_violation_error', '', [], '', '', ''); + + $constraintViolationList = new ConstraintViolationList([$firstConstraintViolation, $secondConstraintViolation]); + + $this->arrangePartially($constraintViolationList); + + $this->adminUserRepository->expects($this->never())->method('add'); + + $createAdminUserHandler = $this->createAdminUserHandler(); + + self::expectException(CreateAdminUserFailedException::class); + self::expectExceptionMessage('first_violation_error' . \PHP_EOL . 'second_violation_error'); + + $createAdminUserHandler($this->createAdminUserMessage()); + } + + private function arrangePartially(ConstraintViolationList $validationErrorsList): void + { + $this->adminUserFactory->expects($this->once())->method('createNew')->willReturn($this->adminUser); + + $this->canonicalizer + ->expects($this->once()) + ->method('canonicalize') + ->with(self::NON_CANONICALIZED_EMAIL) + ->willReturn(self::EMAIL) + ; + + $this->adminUser->expects($this->once())->method('setEmail')->with(self::EMAIL); + $this->adminUser->expects($this->once())->method('setUsername')->with(self::USERNAME); + $this->adminUser->expects($this->once())->method('setPlainPassword')->with(self::PASSWORD); + $this->adminUser->expects($this->once())->method('setFirstName')->with(self::FIRST_NAME); + $this->adminUser->expects($this->once())->method('setLastName')->with(self::LAST_NAME); + $this->adminUser->expects($this->once())->method('setLocaleCode')->with(self::LOCALE_CODE); + $this->adminUser->expects($this->once())->method('setEnabled')->with(self::ENABLED); + + $this->validator + ->expects($this->once()) + ->method('validate') + ->with($this->adminUser, null, self::GROUPS) + ->willReturn($validationErrorsList) + ; + } + + private function createAdminUserHandler(): CreateAdminUserHandler + { + return new CreateAdminUserHandler( + $this->adminUserRepository, + $this->adminUserFactory, + $this->canonicalizer, + $this->validator, + self::GROUPS, + ); + } + + private function createAdminUserMessage(): CreateAdminUser + { + return new CreateAdminUser( + self::NON_CANONICALIZED_EMAIL, + self::USERNAME, + self::FIRST_NAME, + self::LAST_NAME, + self::PASSWORD, + self::LOCALE_CODE, + self::ENABLED, + ); + } +} diff --git a/src/Sylius/Bundle/AdminBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php b/src/Sylius/Bundle/AdminBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php deleted file mode 100644 index cca655a0a9..0000000000 --- a/src/Sylius/Bundle/AdminBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php +++ /dev/null @@ -1,123 +0,0 @@ -markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - - $container = self::bootKernel()->getContainer(); - - /** @var TranslatorInterface $translator */ - $translator = $container->get('translator'); - - /** @var SenderInterface $emailSender */ - $emailSender = $container->get('sylius.email_sender'); - - $adminUser = new AdminUser(); - $adminUser->setEmail('sylius@example.com'); - $adminUser->setPasswordResetToken('my_reset_token'); - - $adminUserRepository = $this->createMock(UserRepositoryInterface::class); - $adminUserRepository - ->method('findOneByEmail') - ->with('sylius@example.com') - ->willReturn($adminUser) - ; - - $resetPasswordEmailHandler = new SendResetPasswordEmailHandler($adminUserRepository, $emailSender); - $resetPasswordEmailHandler(new SendResetPasswordEmail( - 'sylius@example.com', - 'en_US', - )); - - self::assertEmailCount(1); - $email = self::getMailerMessage(); - self::assertEmailAddressContains($email, 'To', 'sylius@example.com'); - self::assertEmailHtmlBodyContains( - $email, - $translator->trans('sylius.email.admin_password_reset.to_reset_your_password', [], null, 'en_US'), - ); - } - - /** @test */ - public function it_sends_password_reset_token_email_with_swiftmailer(): void - { - if (!self::isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment with swiftmailer'); - } - - $container = self::bootKernel()->getContainer(); - - self::setSpoolDirectory($container->getParameter('kernel.cache_dir') . '/spool'); - - /** @var Filesystem $filesystem */ - $filesystem = $container->get('test.filesystem.public'); - - $filesystem->remove(self::getSpoolDirectory()); - - /** @var TranslatorInterface $translator */ - $translator = $container->get('translator'); - - /** @var SenderInterface $emailSender */ - $emailSender = $container->get('sylius.email_sender'); - - $adminUser = new AdminUser(); - $adminUser->setEmail('sylius@example.com'); - $adminUser->setPasswordResetToken('my_reset_token'); - - $adminUserRepository = $this->createMock(UserRepositoryInterface::class); - $adminUserRepository - ->method('findOneByEmail') - ->with('sylius@example.com') - ->willReturn($adminUser) - ; - - $resetPasswordEmailHandler = new SendResetPasswordEmailHandler($adminUserRepository, $emailSender); - $resetPasswordEmailHandler(new SendResetPasswordEmail( - 'sylius@example.com', - 'en_US', - )); - - self::assertSpooledMessagesCountWithRecipient(1, 'sylius@example.com'); - self::assertSpooledMessageWithContentHasRecipient( - $translator->trans('sylius.email.admin_password_reset.to_reset_your_password', [], null, 'en_US'), - 'sylius@example.com', - ); - } - - private static function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } -} diff --git a/src/Sylius/Bundle/AdminBundle/composer.json b/src/Sylius/Bundle/AdminBundle/composer.json index 1eb35a1491..f1d9bdd48d 100644 --- a/src/Sylius/Bundle/AdminBundle/composer.json +++ b/src/Sylius/Bundle/AdminBundle/composer.json @@ -30,22 +30,24 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "knplabs/knp-menu": "^3.1", "knplabs/knp-menu-bundle": "^3.0", - "sylius/core-bundle": "^1.12", - "sylius/ui-bundle": "^1.12", - "symfony/framework-bundle": "^5.4 || ^6.0", + "sylius/core-bundle": "^1.13", + "sylius/ui-bundle": "^1.13", + "symfony/framework-bundle": "^5.4.21 || ^6.4", "symfony/webpack-encore-bundle": "^1.15", "twig/intl-extra": "^2.12 || ^3.4", "twig/twig": "^2.12 || ^3.3" }, "require-dev": { + "matthiasnoback/symfony-dependency-injection-test": "^4.2", + "php-http/message-factory": "^1.0", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/dotenv": "^5.4 || ^6.0", - "php-http/message-factory": "^1.0" + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/dotenv": "^5.4.21 || ^6.4" }, "config": { "allow-plugins": { @@ -54,7 +56,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { diff --git a/src/Sylius/Bundle/AdminBundle/spec/Event/OrderShowMenuBuilderEventSpec.php b/src/Sylius/Bundle/AdminBundle/spec/Event/OrderShowMenuBuilderEventSpec.php index 0add856976..400c24d229 100644 --- a/src/Sylius/Bundle/AdminBundle/spec/Event/OrderShowMenuBuilderEventSpec.php +++ b/src/Sylius/Bundle/AdminBundle/spec/Event/OrderShowMenuBuilderEventSpec.php @@ -16,7 +16,8 @@ namespace spec\Sylius\Bundle\AdminBundle\Event; use Knp\Menu\FactoryInterface; use Knp\Menu\ItemInterface; use PhpSpec\ObjectBehavior; -use SM\StateMachine\StateMachineInterface; +use SM\StateMachine\StateMachineInterface as WinzouStateMachineInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\UiBundle\Menu\Event\MenuBuilderEvent; use Sylius\Component\Core\Model\OrderInterface; @@ -26,7 +27,7 @@ final class OrderShowMenuBuilderEventSpec extends ObjectBehavior FactoryInterface $factory, ItemInterface $menu, OrderInterface $order, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $this->beConstructedWith($factory, $menu, $order, $stateMachine); } @@ -41,8 +42,18 @@ final class OrderShowMenuBuilderEventSpec extends ObjectBehavior $this->getOrder()->shouldReturn($order); } - function it_has_a_state_machine(StateMachineInterface $stateMachine): void + function it_has_a_state_machine(WinzouStateMachineInterface $stateMachine): void { $this->getStateMachine()->shouldReturn($stateMachine); } + + function it_allows_to_pass_the_new_state_machine_abstraction( + FactoryInterface $factory, + ItemInterface $menu, + OrderInterface $order, + StateMachineInterface $newStateMachineAbstraction, + ): void { + $this->beConstructedWith($factory, $menu, $order, $newStateMachineAbstraction); + $this->getStateMachine()->shouldReturn($newStateMachineAbstraction); + } } diff --git a/src/Sylius/Bundle/AdminBundle/spec/EventListener/LocaleListenerSpec.php b/src/Sylius/Bundle/AdminBundle/spec/EventListener/LocaleListenerSpec.php new file mode 100644 index 0000000000..d43b4310d9 --- /dev/null +++ b/src/Sylius/Bundle/AdminBundle/spec/EventListener/LocaleListenerSpec.php @@ -0,0 +1,58 @@ +beConstructedWith($localeUsageChecker); + } + + function it_does_nothing_if_locale_is_not_used( + LocaleUsageCheckerInterface $localeUsageChecker, + LocaleInterface $locale, + ResourceControllerEvent $event, + ): void { + $localeUsageChecker->isUsed('en_US')->willReturn(false); + + $locale->getCode()->willReturn('en_US'); + + $event->getSubject()->willReturn($locale); + $event->stop()->shouldNotBeCalled(); + + $this->preDelete($event); + } + + function it_stops_event_if_locale_is_used( + LocaleUsageCheckerInterface $localeUsageChecker, + LocaleInterface $locale, + ResourceControllerEvent $event, + ): void { + $localeUsageChecker->isUsed('en_US')->willReturn(true); + + $locale->getCode()->willReturn('en_US'); + + $event->getSubject()->willReturn($locale); + $event->stop('sylius.locale.delete.is_used', 'error', [], Response::HTTP_UNPROCESSABLE_ENTITY)->shouldBeCalled(); + + $this->preDelete($event); + } +} diff --git a/src/Sylius/Bundle/AdminBundle/spec/EventListener/ShipmentShipListenerSpec.php b/src/Sylius/Bundle/AdminBundle/spec/EventListener/ShipmentShipListenerSpec.php index 820c7e21b4..d2ddc7df8e 100644 --- a/src/Sylius/Bundle/AdminBundle/spec/EventListener/ShipmentShipListenerSpec.php +++ b/src/Sylius/Bundle/AdminBundle/spec/EventListener/ShipmentShipListenerSpec.php @@ -14,7 +14,8 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\AdminBundle\EventListener; use PhpSpec\ObjectBehavior; -use Sylius\Bundle\AdminBundle\EmailManager\ShipmentEmailManagerInterface; +use Sylius\Bundle\AdminBundle\EmailManager\ShipmentEmailManagerInterface as DeprecatedShipmentEmailManagerInterface; +use Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface; use Sylius\Component\Core\Model\ShipmentInterface; use Symfony\Component\EventDispatcher\GenericEvent; @@ -37,6 +38,20 @@ final class ShipmentShipListenerSpec extends ObjectBehavior $this->sendConfirmationEmail($event); } + function it_sends_a_confirmation_email_by_deprecated_manager( + DeprecatedShipmentEmailManagerInterface $shipmentEmailManager, + GenericEvent $event, + ShipmentInterface $shipment, + ): void { + $this->beConstructedWith($shipmentEmailManager); + + $event->getSubject()->willReturn($shipment); + + $shipmentEmailManager->sendConfirmationEmail($shipment)->shouldBeCalled(); + + $this->sendConfirmationEmail($event); + } + function it_throws_an_invalid_argument_exception_if_an_event_subject_is_not_a_shipment_instance( GenericEvent $event, \stdClass $shipment, diff --git a/src/Sylius/Bundle/AdminBundle/test/config/bundles.php b/src/Sylius/Bundle/AdminBundle/test/config/bundles.php index 6d2563035c..fd67608a3e 100644 --- a/src/Sylius/Bundle/AdminBundle/test/config/bundles.php +++ b/src/Sylius/Bundle/AdminBundle/test/config/bundles.php @@ -19,6 +19,7 @@ return [ Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Sylius\Calendar\SyliusCalendarBundle::class => ['all' => true], + Sylius\Abstraction\StateMachine\SyliusStateMachineAbstractionBundle::class => ['all' => true], Sylius\Bundle\OrderBundle\SyliusOrderBundle::class => ['all' => true], Sylius\Bundle\MoneyBundle\SyliusMoneyBundle::class => ['all' => true], Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle::class => ['all' => true], diff --git a/src/Sylius/Bundle/AdminBundle/test/config/packages/config.yaml b/src/Sylius/Bundle/AdminBundle/test/config/packages/config.yaml index dca50202dc..dcdbff194e 100644 --- a/src/Sylius/Bundle/AdminBundle/test/config/packages/config.yaml +++ b/src/Sylius/Bundle/AdminBundle/test/config/packages/config.yaml @@ -15,6 +15,7 @@ framework: test: ~ mailer: dsn: 'null://null' + workflows: ~ security: firewalls: diff --git a/src/Sylius/Bundle/ApiBundle/ApiPlatform/ApiResourceConfigurationMerger.php b/src/Sylius/Bundle/ApiBundle/ApiPlatform/ApiResourceConfigurationMerger.php index 6ef1d879cd..65c91d6415 100644 --- a/src/Sylius/Bundle/ApiBundle/ApiPlatform/ApiResourceConfigurationMerger.php +++ b/src/Sylius/Bundle/ApiBundle/ApiPlatform/ApiResourceConfigurationMerger.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\ApiPlatform; -/** @experimental */ final class ApiResourceConfigurationMerger implements ApiResourceConfigurationMergerInterface { public function mergeConfigs(...$configs): array diff --git a/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php deleted file mode 100644 index 24aff21d65..0000000000 --- a/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php +++ /dev/null @@ -1,173 +0,0 @@ -formats = $formats; - - return; - } - - @trigger_error(sprintf( - 'Passing an array or an instance of "%s" as 5th parameter of the constructor of "%s" is deprecated since API Platform 2.5, pass an array instead', - FormatsProviderInterface::class, - __CLASS__, - ), \E_USER_DEPRECATED); - $this->formatsProvider = $formats; - } - - public function __invoke(Request $request) - { - $attributes = RequestAttributesExtractor::extractAttributes($request); - - // BC check to be removed in 3.0 - if (null === $this->formatsProvider) { - $formats = $attributes ? $this - ->resourceMetadataFactory - ->create($attributes['resource_class']) - ->getOperationAttribute($attributes, 'output_formats', [], true) : $this->formats; - } else { - $formats = $this->formatsProvider->getFormatsFromAttributes($attributes); - } - - $documentation = new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version); - - return new Response($this->twig->render('@SyliusApi/SwaggerUi/index.html.twig', $this->getContext($request, $documentation) + ['formats' => $formats])); - } - - /** - * Gets the base Twig context. - */ - private function getContext(Request $request, Documentation $documentation): array - { - $context = [ - 'title' => $this->title, - 'description' => $this->description, - 'showWebby' => $this->showWebby, - 'swaggerUiEnabled' => $this->swaggerUiEnabled, - 'reDocEnabled' => $this->reDocEnabled, - 'graphqlEnabled' => $this->graphqlEnabled, - 'graphiQlEnabled' => $this->graphiQlEnabled, - 'graphQlPlaygroundEnabled' => $this->graphQlPlaygroundEnabled, - ]; - - $swaggerContext = ['spec_version' => $request->query->getInt('spec_version', $this->swaggerVersions[0] ?? 2)]; - if ('' !== $baseUrl = $request->getBaseUrl()) { - $swaggerContext['base_url'] = $baseUrl; - } - - $swaggerData = [ - 'url' => $this->urlGenerator->generate('api_doc', ['format' => 'json']), - 'spec' => $this->normalizer->normalize($documentation, 'json', $swaggerContext), - ]; - - $swaggerData['oauth'] = [ - 'enabled' => $this->oauthEnabled, - 'clientId' => $this->oauthClientId, - 'clientSecret' => $this->oauthClientSecret, - 'type' => $this->oauthType, - 'flow' => $this->oauthFlow, - 'tokenUrl' => $this->oauthTokenUrl, - 'authorizationUrl' => $this->oauthAuthorizationUrl, - 'scopes' => $this->oauthScopes, - ]; - - if ($request->isMethodSafe() && null !== $resourceClass = $request->attributes->get('_api_resource_class')) { - $swaggerData['id'] = $request->attributes->get('id'); - $swaggerData['queryParameters'] = $request->query->all(); - - $metadata = $this->resourceMetadataFactory->create($resourceClass); - $swaggerData['shortName'] = $metadata->getShortName(); - - if (null !== $collectionOperationName = $request->attributes->get('_api_collection_operation_name')) { - $swaggerData['operationId'] = sprintf('%s%sCollection', $collectionOperationName, ucfirst($swaggerData['shortName'])); - } elseif (null !== $itemOperationName = $request->attributes->get('_api_item_operation_name')) { - $swaggerData['operationId'] = sprintf('%s%sItem', $itemOperationName, ucfirst($swaggerData['shortName'])); - } elseif (null !== $subresourceOperationContext = $request->attributes->get('_api_subresource_context')) { - $swaggerData['operationId'] = $subresourceOperationContext['operationId']; - } - - [$swaggerData['path'], $swaggerData['method']] = $this->getPathAndMethod($swaggerData); - } - - return $context + ['swagger_data' => $swaggerData]; - } - - private function getPathAndMethod(array $swaggerData): array - { - foreach ($swaggerData['spec']['paths'] as $path => $operations) { - foreach ($operations as $method => $operation) { - if ($operation['operationId'] === $swaggerData['operationId']) { - return [$path, $method]; - } - } - } - - throw new RuntimeException(sprintf('The operation "%s" cannot be found in the Swagger specification.', $swaggerData['operationId'])); - } -} diff --git a/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Routing/CachedRouteNameResolver.php b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Routing/CachedRouteNameResolver.php index 171f5f5ebe..9525ae5dc9 100644 --- a/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Routing/CachedRouteNameResolver.php +++ b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Routing/CachedRouteNameResolver.php @@ -19,8 +19,6 @@ use Psr\Cache\CacheItemPoolInterface; use Sylius\Bundle\ApiBundle\Provider\PathPrefixProviderInterface; /** - * @experimental - * * This class is based on src/Bridge/Symfony/Routing/CachedRouteNameResolver.php, but has added logic for matching /shop, /admin prefixes */ final class CachedRouteNameResolver implements RouteNameResolverInterface diff --git a/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Routing/RouteNameResolver.php b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Routing/RouteNameResolver.php index 2d29151989..17e2de8a4f 100644 --- a/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Routing/RouteNameResolver.php +++ b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Bridge/Symfony/Routing/RouteNameResolver.php @@ -20,8 +20,6 @@ use Sylius\Bundle\ApiBundle\Provider\PathPrefixProviderInterface; use Symfony\Component\Routing\RouterInterface; /** - * @experimental - * * This class is based on src/Bridge/Symfony/Routing/RouteNameResolver.php, but has added logic for matching /shop, /admin prefixes */ final class RouteNameResolver implements RouteNameResolverInterface diff --git a/src/Sylius/Bundle/ApiBundle/ApiPlatform/Factory/MergingExtractorResourceMetadataFactory.php b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Factory/MergingExtractorResourceMetadataFactory.php index a39c76702d..41c0d87a57 100644 --- a/src/Sylius/Bundle/ApiBundle/ApiPlatform/Factory/MergingExtractorResourceMetadataFactory.php +++ b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Factory/MergingExtractorResourceMetadataFactory.php @@ -20,7 +20,6 @@ use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; use Sylius\Bundle\ApiBundle\ApiPlatform\ResourceMetadataPropertyValueResolver; /** - * @experimental * This class is overwriting ApiPlatform ExtractorResourceMetadataFactory to allow yaml files to be merged into api platform config */ final class MergingExtractorResourceMetadataFactory implements ResourceMetadataFactoryInterface diff --git a/src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/LegacyResourceMetadataMerger.php b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/LegacyResourceMetadataMerger.php new file mode 100644 index 0000000000..d142ad2d12 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/LegacyResourceMetadataMerger.php @@ -0,0 +1,51 @@ + $value) { + if ('properties' === $key) { + continue; + } + + if (is_array($value) && str_contains($key, 'Operations')) { + foreach ($value as $operationKey => $operationData) { + if (isset($operationData['enabled']) && false === $operationData['enabled']) { + unset($oldMetadata[$key][$operationKey]); + + continue; + } + + $oldMetadata[$key][$operationKey] = array_merge( + $oldMetadata[$key][$operationKey] ?? [], + $operationData, + ); + } + + continue; + } + + $oldMetadata[$key] = $value; + } + + return $oldMetadata; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/MetadataMergerInterface.php b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/MetadataMergerInterface.php new file mode 100644 index 0000000000..1b7d532d5b --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/ApiPlatform/Metadata/Merger/MetadataMergerInterface.php @@ -0,0 +1,19 @@ +resources)) { + return $this->resources; + } + + $this->resources = []; + foreach ($this->paths as $path) { + $this->extractPath($path); + } + + return $this->resources; + } + + /** + * @inheritdoc + */ + public function getProperties(): array + { + if (!empty($this->properties)) { + return $this->properties; + } + + $this->properties = []; + foreach ($this->paths as $path) { + $this->extractPath($path); + } + + return $this->properties; + } + + protected function extractPath(string $path): void + { + try { + /** @var \SimpleXMLElement $xml */ + $xml = simplexml_import_dom(XmlUtils::loadFile($path, XmlExtractor::RESOURCE_SCHEMA)); + } catch (\InvalidArgumentException $e) { + // Test if this is a new resource + try { + $xml = XmlUtils::loadFile($path, XmlResourceExtractor::SCHEMA); + + return; + } catch (\InvalidArgumentException) { + try { + $xml = XmlUtils::loadFile($path, XmlPropertyExtractor::SCHEMA); + + return; + } catch (\InvalidArgumentException) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + } + } + + foreach ($xml->resource as $resource) { + $resourceClass = $this->resolve((string) $resource['class']); + $resourceMetadata = $this->buildResource($resource); + + if (null !== $this->merger && isset($this->resources[$resourceClass])) { + $this->resources[$resourceClass] = $this->merger->merge( + $this->resources[$resourceClass], + $resourceMetadata, + ); + $this->properties[$resourceClass] = array_merge( + $this->properties[$resourceClass], + $resourceMetadata['properties'], + ); + + continue; + } + + $this->resources[$resourceClass] = $resourceMetadata; + $this->properties[$resourceClass] = $this->resources[$resourceClass]['properties']; + } + } + + private function buildResource(\SimpleXMLElement $resource): array + { + return [ + 'shortName' => $this->phpizeAttribute($resource, 'shortName', 'string'), + 'description' => $this->phpizeAttribute($resource, 'description', 'string'), + 'iri' => $this->phpizeAttribute($resource, 'iri', 'string'), + 'itemOperations' => $this->extractOperations($resource, 'itemOperation'), + 'collectionOperations' => $this->extractOperations($resource, 'collectionOperation'), + 'subresourceOperations' => $this->extractOperations($resource, 'subresourceOperation'), + 'graphql' => $this->extractOperations($resource, 'operation'), + 'attributes' => $this->extractAttributes($resource, 'attribute') ?: null, + 'properties' => $this->extractProperties($resource) ?: null, + ]; + } + + /** + * Returns the array containing configured operations. Returns NULL if there is no operation configuration. + */ + private function extractOperations(\SimpleXMLElement $resource, string $operationType): ?array + { + $graphql = 'operation' === $operationType; + if (!$graphql && $legacyOperations = $this->extractAttributes($resource, $operationType)) { + trigger_deprecation( + 'api-platform/core', + '2.1', + 'Configuring "%1$s" tags without using a parent "%1$ss" tag is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3', + $operationType, + ); + + return $legacyOperations; + } + + $operationsParent = $graphql ? 'graphql' : "{$operationType}s"; + if (!isset($resource->{$operationsParent})) { + return null; + } + + return $this->extractAttributes($resource->{$operationsParent}, $operationType, true); + } + + /** + * Recursively transforms an attribute structure into an associative array. + */ + private function extractAttributes(\SimpleXMLElement $resource, string $elementName, bool $topLevel = false): array + { + $attributes = []; + foreach ($resource->{$elementName} as $attribute) { + $value = isset($attribute->attribute[0]) ? $this->extractAttributes($attribute, 'attribute') : $this->phpizeContent($attribute); + // allow empty operations definition, like + if ($topLevel && '' === $value) { + $value = []; + } + if (isset($attribute['name'])) { + $attributes[(string) $attribute['name']] = $value; + } else { + $attributes[] = $value; + } + } + + return $attributes; + } + + /** + * Gets metadata of a property. + */ + private function extractProperties(\SimpleXMLElement $resource): array + { + $properties = []; + foreach ($resource->property as $property) { + $properties[(string) $property['name']] = [ + 'description' => $this->phpizeAttribute($property, 'description', 'string'), + 'readable' => $this->phpizeAttribute($property, 'readable', 'bool'), + 'writable' => $this->phpizeAttribute($property, 'writable', 'bool'), + 'readableLink' => $this->phpizeAttribute($property, 'readableLink', 'bool'), + 'writableLink' => $this->phpizeAttribute($property, 'writableLink', 'bool'), + 'required' => $this->phpizeAttribute($property, 'required', 'bool'), + 'identifier' => $this->phpizeAttribute($property, 'identifier', 'bool'), + 'iri' => $this->phpizeAttribute($property, 'iri', 'string'), + 'attributes' => $this->extractAttributes($property, 'attribute'), + 'subresource' => $property->subresource ? [ + 'collection' => $this->phpizeAttribute($property->subresource, 'collection', 'bool'), + 'resourceClass' => $this->resolve($this->phpizeAttribute($property->subresource, 'resourceClass', 'string')), + 'maxDepth' => $this->phpizeAttribute($property->subresource, 'maxDepth', 'integer'), + ] : null, + ]; + } + + return $properties; + } + + /** + * Transforms an XML attribute's value in a PHP value. + */ + private function phpizeAttribute(\SimpleXMLElement $array, string $key, string $type): bool|int|string|null + { + if (!isset($array[$key])) { + return null; + } + + return match ($type) { + 'string' => (string) $array[$key], + 'integer' => (int) $array[$key], + 'bool' => (bool) XmlUtils::phpize($array[$key]), + default => null, + }; + } + + /** + * Transforms an XML element's content in a PHP value. + */ + private function phpizeContent(\SimpleXMLElement $array) + { + $type = $array['type'] ?? null; + $value = (string) $array; + + switch ($type) { + case 'string': + return $value; + case 'constant': + return \constant($value); + default: + return XmlUtils::phpize($value); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/ApiPlatform/ResourceMetadataPropertyValueResolver.php b/src/Sylius/Bundle/ApiBundle/ApiPlatform/ResourceMetadataPropertyValueResolver.php index 753cb5770e..db4ad31a7f 100644 --- a/src/Sylius/Bundle/ApiBundle/ApiPlatform/ResourceMetadataPropertyValueResolver.php +++ b/src/Sylius/Bundle/ApiBundle/ApiPlatform/ResourceMetadataPropertyValueResolver.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\ApiPlatform; use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; -/** @experimental */ final class ResourceMetadataPropertyValueResolver implements ResourceMetadataPropertyValueResolverInteface { public function __construct(private ApiResourceConfigurationMergerInterface $apiResourceConfigurationMerger) diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicator.php b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicator.php new file mode 100644 index 0000000000..d5e1c63046 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicator.php @@ -0,0 +1,38 @@ +setArchivedAt($this->calendar->now()); + + return $data; + } + + public function restore(PromotionInterface $data): PromotionInterface + { + $data->setArchivedAt(null); + + return $data; + } +} diff --git a/src/Sylius/Component/Core/Test/Factory/TestPromotionFactoryInterface.php b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicatorInterface.php similarity index 50% rename from src/Sylius/Component/Core/Test/Factory/TestPromotionFactoryInterface.php rename to src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicatorInterface.php index d33357dde6..6491286826 100644 --- a/src/Sylius/Component/Core/Test/Factory/TestPromotionFactoryInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingPromotionApplicatorInterface.php @@ -11,14 +11,13 @@ declare(strict_types=1); -namespace Sylius\Component\Core\Test\Factory; +namespace Sylius\Bundle\ApiBundle\Applicator; -use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\PromotionInterface; -interface TestPromotionFactoryInterface +interface ArchivingPromotionApplicatorInterface { - public function create(string $name): PromotionInterface; + public function archive(PromotionInterface $data): PromotionInterface; - public function createForChannel(string $name, ChannelInterface $channel): PromotionInterface; + public function restore(PromotionInterface $data): PromotionInterface; } diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingShippingMethodApplicator.php b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingShippingMethodApplicator.php index daa6354f4b..87198eab5f 100644 --- a/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingShippingMethodApplicator.php +++ b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingShippingMethodApplicator.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\Applicator; use Sylius\Calendar\Provider\DateTimeProviderInterface; use Sylius\Component\Core\Model\ShippingMethodInterface; -/** @experimental */ final class ArchivingShippingMethodApplicator implements ArchivingShippingMethodApplicatorInterface { public function __construct(private DateTimeProviderInterface $calendar) diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingShippingMethodApplicatorInterface.php b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingShippingMethodApplicatorInterface.php index 6a33c73249..cbccf692a6 100644 --- a/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingShippingMethodApplicatorInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Applicator/ArchivingShippingMethodApplicatorInterface.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Applicator; use Sylius\Component\Core\Model\ShippingMethodInterface; -/** @experimental */ interface ArchivingShippingMethodApplicatorInterface { public function archive(ShippingMethodInterface $data): ShippingMethodInterface; diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/OrderStateMachineTransitionApplicator.php b/src/Sylius/Bundle/ApiBundle/Applicator/OrderStateMachineTransitionApplicator.php index 6fc061b910..38e2aad650 100644 --- a/src/Sylius/Bundle/ApiBundle/Applicator/OrderStateMachineTransitionApplicator.php +++ b/src/Sylius/Bundle/ApiBundle/Applicator/OrderStateMachineTransitionApplicator.php @@ -14,14 +14,27 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Applicator; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; +use Sylius\Bundle\ApiBundle\Exception\StateMachineTransitionFailedException; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Order\OrderTransitions; -/** @experimental */ final class OrderStateMachineTransitionApplicator implements OrderStateMachineTransitionApplicatorInterface { - public function __construct(private StateMachineFactoryInterface $stateMachineFactory) + public function __construct(private StateMachineFactoryInterface|StateMachineInterface $stateMachineFactory) { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + trigger_deprecation( + 'sylius/api-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the first argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + StateMachineFactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function cancel(OrderInterface $data): OrderInterface @@ -33,7 +46,21 @@ final class OrderStateMachineTransitionApplicator implements OrderStateMachineTr private function applyTransition(OrderInterface $order, string $transition): void { - $stateMachine = $this->stateMachineFactory->get($order, OrderTransitions::GRAPH); - $stateMachine->apply($transition); + $stateMachine = $this->getStateMachine(); + + if (false === $stateMachine->can($order, OrderTransitions::GRAPH, $transition)) { + throw new StateMachineTransitionFailedException('Cannot cancel the order.'); + } + + $stateMachine->apply($order, OrderTransitions::GRAPH, $transition); + } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; } } diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/OrderStateMachineTransitionApplicatorInterface.php b/src/Sylius/Bundle/ApiBundle/Applicator/OrderStateMachineTransitionApplicatorInterface.php index 311968413b..b7f849e39c 100644 --- a/src/Sylius/Bundle/ApiBundle/Applicator/OrderStateMachineTransitionApplicatorInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Applicator/OrderStateMachineTransitionApplicatorInterface.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Applicator; use Sylius\Component\Core\Model\OrderInterface; -/** @experimental */ interface OrderStateMachineTransitionApplicatorInterface { public function cancel(OrderInterface $data): OrderInterface; diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/PaymentStateMachineTransitionApplicator.php b/src/Sylius/Bundle/ApiBundle/Applicator/PaymentStateMachineTransitionApplicator.php index 67dc0f7a62..ccbe66f3f9 100644 --- a/src/Sylius/Bundle/ApiBundle/Applicator/PaymentStateMachineTransitionApplicator.php +++ b/src/Sylius/Bundle/ApiBundle/Applicator/PaymentStateMachineTransitionApplicator.php @@ -14,14 +14,27 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Applicator; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; +use Sylius\Bundle\ApiBundle\Exception\StateMachineTransitionFailedException; use Sylius\Component\Payment\Model\PaymentInterface; use Sylius\Component\Payment\PaymentTransitions; -/** @experimental */ final class PaymentStateMachineTransitionApplicator implements PaymentStateMachineTransitionApplicatorInterface { - public function __construct(private StateMachineFactoryInterface $stateMachineFactory) + public function __construct(private StateMachineFactoryInterface|StateMachineInterface $stateMachineFactory) { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + trigger_deprecation( + 'sylius/api-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the first argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + StateMachineFactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function complete(PaymentInterface $data): PaymentInterface @@ -33,7 +46,21 @@ final class PaymentStateMachineTransitionApplicator implements PaymentStateMachi private function applyTransition(PaymentInterface $payment, string $transition): void { - $stateMachine = $this->stateMachineFactory->get($payment, PaymentTransitions::GRAPH); - $stateMachine->apply($transition); + $stateMachine = $this->getStateMachine(); + + if (false === $stateMachine->can($payment, PaymentTransitions::GRAPH, $transition)) { + throw new StateMachineTransitionFailedException('Cannot complete the payment.'); + } + + $stateMachine->apply($payment, PaymentTransitions::GRAPH, $transition); + } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; } } diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/PaymentStateMachineTransitionApplicatorInterface.php b/src/Sylius/Bundle/ApiBundle/Applicator/PaymentStateMachineTransitionApplicatorInterface.php index 55e35cdfce..4193a7f7f4 100644 --- a/src/Sylius/Bundle/ApiBundle/Applicator/PaymentStateMachineTransitionApplicatorInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Applicator/PaymentStateMachineTransitionApplicatorInterface.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Applicator; use Sylius\Component\Payment\Model\PaymentInterface; -/** @experimental */ interface PaymentStateMachineTransitionApplicatorInterface { public function complete(PaymentInterface $data): PaymentInterface; diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/ProductReviewStateMachineTransitionApplicator.php b/src/Sylius/Bundle/ApiBundle/Applicator/ProductReviewStateMachineTransitionApplicator.php index 6b4105371f..a4d853a85a 100644 --- a/src/Sylius/Bundle/ApiBundle/Applicator/ProductReviewStateMachineTransitionApplicator.php +++ b/src/Sylius/Bundle/ApiBundle/Applicator/ProductReviewStateMachineTransitionApplicator.php @@ -14,14 +14,27 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Applicator; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; +use Sylius\Bundle\ApiBundle\Exception\StateMachineTransitionFailedException; use Sylius\Component\Core\ProductReviewTransitions; use Sylius\Component\Review\Model\ReviewInterface; -/** @experimental */ final class ProductReviewStateMachineTransitionApplicator implements ProductReviewStateMachineTransitionApplicatorInterface { - public function __construct(private StateMachineFactoryInterface $stateMachineFactory) + public function __construct(private StateMachineFactoryInterface|StateMachineInterface $stateMachineFactory) { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + trigger_deprecation( + 'sylius/api-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the first argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + StateMachineFactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function accept(ReviewInterface $data): ReviewInterface @@ -40,7 +53,21 @@ final class ProductReviewStateMachineTransitionApplicator implements ProductRevi private function applyTransition(ReviewInterface $review, string $transition): void { - $stateMachine = $this->stateMachineFactory->get($review, ProductReviewTransitions::GRAPH); - $stateMachine->apply($transition); + $stateMachine = $this->getStateMachine(); + + if (false === $stateMachine->can($review, ProductReviewTransitions::GRAPH, $transition)) { + throw new StateMachineTransitionFailedException(sprintf('Cannot %s the product review.', $transition)); + } + + $stateMachine->apply($review, ProductReviewTransitions::GRAPH, $transition); + } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; } } diff --git a/src/Sylius/Bundle/ApiBundle/Applicator/ProductReviewStateMachineTransitionApplicatorInterface.php b/src/Sylius/Bundle/ApiBundle/Applicator/ProductReviewStateMachineTransitionApplicatorInterface.php index 65ff714cdd..c3119a79e6 100644 --- a/src/Sylius/Bundle/ApiBundle/Applicator/ProductReviewStateMachineTransitionApplicatorInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Applicator/ProductReviewStateMachineTransitionApplicatorInterface.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Applicator; use Sylius\Component\Review\Model\ReviewInterface; -/** @experimental */ interface ProductReviewStateMachineTransitionApplicatorInterface { public function accept(ReviewInterface $data): ReviewInterface; diff --git a/src/Sylius/Bundle/ApiBundle/Assigner/OrderPromotionCodeAssigner.php b/src/Sylius/Bundle/ApiBundle/Assigner/OrderPromotionCodeAssigner.php index 39b3a46d09..e8a487692c 100644 --- a/src/Sylius/Bundle/ApiBundle/Assigner/OrderPromotionCodeAssigner.php +++ b/src/Sylius/Bundle/ApiBundle/Assigner/OrderPromotionCodeAssigner.php @@ -19,7 +19,6 @@ use Sylius\Component\Promotion\Model\PromotionCouponInterface; use Sylius\Component\Promotion\Repository\PromotionCouponRepositoryInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class OrderPromotionCodeAssigner implements OrderPromotionCodeAssignerInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/Assigner/OrderPromotionCodeAssignerInterface.php b/src/Sylius/Bundle/ApiBundle/Assigner/OrderPromotionCodeAssignerInterface.php index f62cf21f4c..dc11fa998b 100644 --- a/src/Sylius/Bundle/ApiBundle/Assigner/OrderPromotionCodeAssignerInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Assigner/OrderPromotionCodeAssignerInterface.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Assigner; use Sylius\Component\Core\Model\OrderInterface; -/** @experimental */ interface OrderPromotionCodeAssignerInterface { public function assign(OrderInterface $cart, ?string $couponCode = null): OrderInterface; diff --git a/src/Sylius/Bundle/ApiBundle/Attribute/AsCommandDataTransformer.php b/src/Sylius/Bundle/ApiBundle/Attribute/AsCommandDataTransformer.php new file mode 100644 index 0000000000..dc0fa46691 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Attribute/AsCommandDataTransformer.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Attribute/AsDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/Attribute/AsDocumentationModifier.php new file mode 100644 index 0000000000..fa1e31b68a --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Attribute/AsDocumentationModifier.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Attribute/AsPaymentConfigurationProvider.php b/src/Sylius/Bundle/ApiBundle/Attribute/AsPaymentConfigurationProvider.php new file mode 100644 index 0000000000..4a56f977f0 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Attribute/AsPaymentConfigurationProvider.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Behat/Extension/SyliusApiBundleExtension.php b/src/Sylius/Bundle/ApiBundle/Behat/Extension/SyliusApiBundleExtension.php index 0f34bb0881..cb088753e6 100644 --- a/src/Sylius/Bundle/ApiBundle/Behat/Extension/SyliusApiBundleExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Behat/Extension/SyliusApiBundleExtension.php @@ -24,8 +24,6 @@ use Symfony\Component\DependencyInjection\Reference; /** * This extension disables javascript session when running api scenarios - * - * @experimental */ final class SyliusApiBundleExtension implements Extension { diff --git a/src/Sylius/Bundle/ApiBundle/Behat/Tester/ApiScenarioEventDispatchingScenarioTester.php b/src/Sylius/Bundle/ApiBundle/Behat/Tester/ApiScenarioEventDispatchingScenarioTester.php index 06dd703521..36a1fae5cd 100644 --- a/src/Sylius/Bundle/ApiBundle/Behat/Tester/ApiScenarioEventDispatchingScenarioTester.php +++ b/src/Sylius/Bundle/ApiBundle/Behat/Tester/ApiScenarioEventDispatchingScenarioTester.php @@ -23,7 +23,6 @@ use Behat\Testwork\Tester\Result\TestResult; use Behat\Testwork\Tester\Setup\Setup; use Behat\Testwork\Tester\Setup\Teardown; -/** @experimental */ final class ApiScenarioEventDispatchingScenarioTester implements ScenarioTester { public function __construct(private ScenarioTester $baseTester) diff --git a/src/Sylius/Bundle/ApiBundle/Changer/PaymentMethodChanger.php b/src/Sylius/Bundle/ApiBundle/Changer/PaymentMethodChanger.php index 5e222e99ad..f6514e91d7 100644 --- a/src/Sylius/Bundle/ApiBundle/Changer/PaymentMethodChanger.php +++ b/src/Sylius/Bundle/ApiBundle/Changer/PaymentMethodChanger.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Changer; +use Sylius\Bundle\ApiBundle\Exception\PaymentMethodCannotBeChangedException; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\Component\Core\Repository\PaymentMethodRepositoryInterface; @@ -20,7 +21,6 @@ use Sylius\Component\Core\Repository\PaymentRepositoryInterface; use Sylius\Component\Payment\Model\PaymentInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class PaymentMethodChanger implements PaymentMethodChangerInterface { public function __construct( @@ -54,6 +54,6 @@ final class PaymentMethodChanger implements PaymentMethodChangerInterface return $order; } - throw new \InvalidArgumentException('Payment method can not be set'); + throw new PaymentMethodCannotBeChangedException(); } } diff --git a/src/Sylius/Bundle/ApiBundle/Changer/PaymentMethodChangerInterface.php b/src/Sylius/Bundle/ApiBundle/Changer/PaymentMethodChangerInterface.php index 91deb5ddda..1a0541c7ac 100644 --- a/src/Sylius/Bundle/ApiBundle/Changer/PaymentMethodChangerInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Changer/PaymentMethodChangerInterface.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Changer; use Sylius\Component\Core\Model\OrderInterface; -/** @experimental */ interface PaymentMethodChangerInterface { public function changePaymentMethod( diff --git a/src/Sylius/Bundle/ApiBundle/Command/Account/ChangePaymentMethod.php b/src/Sylius/Bundle/ApiBundle/Command/Account/ChangePaymentMethod.php index 7ca77c3204..1b89391606 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Account/ChangePaymentMethod.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Account/ChangePaymentMethod.php @@ -17,7 +17,6 @@ use Sylius\Bundle\ApiBundle\Command\IriToIdentifierConversionAwareInterface; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; use Sylius\Bundle\ApiBundle\Command\SubresourceIdAwareInterface; -/** @experimental */ class ChangePaymentMethod implements OrderTokenValueAwareInterface, SubresourceIdAwareInterface, IriToIdentifierConversionAwareInterface { /** @var string|null */ @@ -30,16 +29,8 @@ class ChangePaymentMethod implements OrderTokenValueAwareInterface, SubresourceI */ public $paymentId; - /** - * @immutable - * - * @var string - */ - public $paymentMethodCode; - - public function __construct(string $paymentMethodCode) + public function __construct(public string $paymentMethodCode) { - $this->paymentMethodCode = $paymentMethodCode; } public function getOrderTokenValue(): ?string diff --git a/src/Sylius/Bundle/ApiBundle/Command/Account/ChangeShopUserPassword.php b/src/Sylius/Bundle/ApiBundle/Command/Account/ChangeShopUserPassword.php index 73775167a7..24a062288a 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Account/ChangeShopUserPassword.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Account/ChangeShopUserPassword.php @@ -15,38 +15,13 @@ namespace Sylius\Bundle\ApiBundle\Command\Account; use Sylius\Bundle\ApiBundle\Command\ShopUserIdAwareInterface; -/** @experimental */ class ChangeShopUserPassword implements ShopUserIdAwareInterface { /** @var mixed|null */ public $shopUserId; - /** - * @immutable - * - * @var string|null - */ - public $newPassword; - - /** - * @immutable - * - * @var string|null - */ - public $confirmNewPassword; - - /** - * @immutable - * - * @var string|null - */ - public $currentPassword; - - public function __construct(?string $newPassword, ?string $confirmNewPassword, ?string $currentPassword) + public function __construct(public string $newPassword, public string $confirmNewPassword, public string $currentPassword) { - $this->newPassword = $newPassword; - $this->confirmNewPassword = $confirmNewPassword; - $this->currentPassword = $currentPassword; } public function getShopUserId() diff --git a/src/Sylius/Bundle/ApiBundle/Command/Account/RegisterShopUser.php b/src/Sylius/Bundle/ApiBundle/Command/Account/RegisterShopUser.php index 27a11cf356..e3e5ae19fb 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Account/RegisterShopUser.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Account/RegisterShopUser.php @@ -16,64 +16,19 @@ namespace Sylius\Bundle\ApiBundle\Command\Account; use Sylius\Bundle\ApiBundle\Command\ChannelCodeAwareInterface; use Sylius\Bundle\ApiBundle\Command\LocaleCodeAwareInterface; -/** - * @experimental - */ class RegisterShopUser implements ChannelCodeAwareInterface, LocaleCodeAwareInterface { - /** - * @immutable - * - * @var string - */ - public $firstName; + public ?string $channelCode = null; - /** - * @immutable - * - * @var string - */ - public $lastName; - - /** - * @immutable - * - * @var string - */ - public $email; - - /** - * @immutable - * - * @var string - */ - public $password; - - /** - * @immutable - * - * @var bool - */ - public $subscribedToNewsletter; - - /** @var string|null */ - public $channelCode; - - /** @var string|null */ - public $localeCode; + public ?string $localeCode = null; public function __construct( - string $firstName, - string $lastName, - string $email, - string $password, - bool $subscribedToNewsletter = false, + public readonly string $firstName, + public readonly string $lastName, + public readonly string $email, + public readonly string $password, + public readonly bool $subscribedToNewsletter = false, ) { - $this->firstName = $firstName; - $this->lastName = $lastName; - $this->email = $email; - $this->password = $password; - $this->subscribedToNewsletter = $subscribedToNewsletter; } public function getChannelCode(): string diff --git a/src/Sylius/Bundle/ApiBundle/Command/Account/RequestResetPasswordToken.php b/src/Sylius/Bundle/ApiBundle/Command/Account/RequestResetPasswordToken.php index 2ae7c3a1c6..0b3307c9f8 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Account/RequestResetPasswordToken.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Account/RequestResetPasswordToken.php @@ -17,21 +17,17 @@ use Sylius\Bundle\ApiBundle\Command\ChannelCodeAwareInterface; use Sylius\Bundle\ApiBundle\Command\IriToIdentifierConversionAwareInterface; use Sylius\Bundle\ApiBundle\Command\LocaleCodeAwareInterface; -/** @experimental */ -class RequestResetPasswordToken implements ChannelCodeAwareInterface, LocaleCodeAwareInterface, IriToIdentifierConversionAwareInterface +class RequestResetPasswordToken implements + ChannelCodeAwareInterface, + LocaleCodeAwareInterface, + IriToIdentifierConversionAwareInterface { - /** @var string */ - public $email; + public ?string $channelCode = null; - /** @var string|null */ - public $channelCode; + public ?string $localeCode = null; - /** @var string|null */ - public $localeCode; - - public function __construct(string $email) + public function __construct(public readonly string $email) { - $this->email = $email; } public function getEmail(): string diff --git a/src/Sylius/Bundle/ApiBundle/Command/Account/ResendVerificationEmail.php b/src/Sylius/Bundle/ApiBundle/Command/Account/ResendVerificationEmail.php index 41c57006f4..c8ab4243db 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Account/ResendVerificationEmail.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Account/ResendVerificationEmail.php @@ -18,25 +18,18 @@ use Sylius\Bundle\ApiBundle\Command\IriToIdentifierConversionAwareInterface; use Sylius\Bundle\ApiBundle\Command\LocaleCodeAwareInterface; use Sylius\Bundle\ApiBundle\Command\ShopUserIdAwareInterface; -/** @experimental */ -class ResendVerificationEmail implements ShopUserIdAwareInterface, ChannelCodeAwareInterface, LocaleCodeAwareInterface, IriToIdentifierConversionAwareInterface +class ResendVerificationEmail implements + ShopUserIdAwareInterface, + ChannelCodeAwareInterface, + LocaleCodeAwareInterface, + IriToIdentifierConversionAwareInterface { /** @var string|int|null */ public $shopUserId; - /** - * @immutable - * - * @var string|null - */ - public $channelCode; + public ?string $channelCode = null; - /** - * @immutable - * - * @var string|null - */ - public $localeCode; + public ?string $localeCode = null; public function getChannelCode(): string { diff --git a/src/Sylius/Bundle/ApiBundle/Command/Account/ResetPassword.php b/src/Sylius/Bundle/ApiBundle/Command/Account/ResetPassword.php index 1c69fca113..fa396ad925 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Account/ResetPassword.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Account/ResetPassword.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command\Account; -/** @experimental */ class ResetPassword { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/Command/Account/SendAccountRegistrationEmail.php b/src/Sylius/Bundle/ApiBundle/Command/Account/SendAccountRegistrationEmail.php index b92fed382b..de0b42b1c9 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Account/SendAccountRegistrationEmail.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Account/SendAccountRegistrationEmail.php @@ -13,11 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command\Account; -/** - * @experimental - * - * @immutable - */ +/** @immutable */ class SendAccountRegistrationEmail { /** @var string */ diff --git a/src/Sylius/Bundle/ApiBundle/Command/Account/SendAccountVerificationEmail.php b/src/Sylius/Bundle/ApiBundle/Command/Account/SendAccountVerificationEmail.php index a602cfb72f..8e01920b75 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Account/SendAccountVerificationEmail.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Account/SendAccountVerificationEmail.php @@ -13,11 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command\Account; -/** - * @experimental - * - * @immutable - */ +/** @immutable */ class SendAccountVerificationEmail { /** @var string */ diff --git a/src/Sylius/Bundle/ApiBundle/Command/Account/SendResetPasswordEmail.php b/src/Sylius/Bundle/ApiBundle/Command/Account/SendResetPasswordEmail.php index d06f8cdafa..2c649f6804 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Account/SendResetPasswordEmail.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Account/SendResetPasswordEmail.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command\Account; -/** @experimental */ class SendResetPasswordEmail { /** @var string */ diff --git a/src/Sylius/Bundle/ApiBundle/Command/Account/VerifyCustomerAccount.php b/src/Sylius/Bundle/ApiBundle/Command/Account/VerifyCustomerAccount.php index c24a362563..d66e38abc1 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Account/VerifyCustomerAccount.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Account/VerifyCustomerAccount.php @@ -13,18 +13,36 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command\Account; -/** - * @experimental - * - * @immutable - */ -class VerifyCustomerAccount -{ - /** @var string */ - public $token; +use Sylius\Bundle\ApiBundle\Command\ChannelCodeAwareInterface; +use Sylius\Bundle\ApiBundle\Command\LocaleCodeAwareInterface; - public function __construct(string $token) +/** @immutable */ +class VerifyCustomerAccount implements ChannelCodeAwareInterface, LocaleCodeAwareInterface +{ + public function __construct( + public string $token, + private ?string $localeCode = null, + private ?string $channelCode = null, + ) { + } + + public function getChannelCode(): ?string { - $this->token = $token; + return $this->channelCode; + } + + public function setChannelCode(?string $channelCode): void + { + $this->channelCode = $channelCode; + } + + public function getLocaleCode(): ?string + { + return $this->localeCode; + } + + public function setLocaleCode(?string $localeCode): void + { + $this->localeCode = $localeCode; } } diff --git a/src/Sylius/Bundle/ApiBundle/Command/Cart/AddItemToCart.php b/src/Sylius/Bundle/ApiBundle/Command/Cart/AddItemToCart.php index 1cc69210b3..e19af88eef 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Cart/AddItemToCart.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Cart/AddItemToCart.php @@ -16,30 +16,13 @@ namespace Sylius\Bundle\ApiBundle\Command\Cart; use Sylius\Bundle\ApiBundle\Command\IriToIdentifierConversionAwareInterface; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; -/** @experimental */ class AddItemToCart implements OrderTokenValueAwareInterface, IriToIdentifierConversionAwareInterface { /** @var string|null */ public $orderTokenValue; - /** - * @immutable - * - * @var string - */ - public $productVariantCode; - - /** - * @immutable - * - * @var int - */ - public $quantity; - - public function __construct(string $productVariantCode, int $quantity) + public function __construct(public string $productVariantCode, public int $quantity) { - $this->productVariantCode = $productVariantCode; - $this->quantity = $quantity; } public static function createFromData(string $tokenValue, string $productVariantCode, int $quantity): self diff --git a/src/Sylius/Bundle/ApiBundle/Command/Cart/BlameCart.php b/src/Sylius/Bundle/ApiBundle/Command/Cart/BlameCart.php index 369343cd98..f2624659c4 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Cart/BlameCart.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Cart/BlameCart.php @@ -13,26 +13,9 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command\Cart; -/** @experimental */ class BlameCart { - /** - * @immutable - * - * @var string - */ - public $shopUserEmail; - - /** - * @immutable - * - * @var string - */ - public $orderTokenValue; - - public function __construct(string $shopUserEmail, string $orderTokenValue) + public function __construct(public string $shopUserEmail, public string $orderTokenValue) { - $this->shopUserEmail = $shopUserEmail; - $this->orderTokenValue = $orderTokenValue; } } diff --git a/src/Sylius/Bundle/ApiBundle/Command/Cart/ChangeItemQuantityInCart.php b/src/Sylius/Bundle/ApiBundle/Command/Cart/ChangeItemQuantityInCart.php index 36acbbf384..b6d9931a90 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Cart/ChangeItemQuantityInCart.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Cart/ChangeItemQuantityInCart.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\Command\Cart; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; use Sylius\Bundle\ApiBundle\Command\SubresourceIdAwareInterface; -/** @experimental */ class ChangeItemQuantityInCart implements OrderTokenValueAwareInterface, SubresourceIdAwareInterface { /** @var string|null */ @@ -25,16 +24,8 @@ class ChangeItemQuantityInCart implements OrderTokenValueAwareInterface, Subreso /** @var string|null */ public $orderItemId; - /** - * @immutable - * - * @var int - */ - public $quantity; - - public function __construct(int $quantity) + public function __construct(public int $quantity) { - $this->quantity = $quantity; } public static function createFromData(string $tokenValue, string $orderItemId, int $quantity): self diff --git a/src/Sylius/Bundle/ApiBundle/Command/Cart/InformAboutCartRecalculation.php b/src/Sylius/Bundle/ApiBundle/Command/Cart/InformAboutCartRecalculation.php index 474318d65c..178869e4a2 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Cart/InformAboutCartRecalculation.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Cart/InformAboutCartRecalculation.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command\Cart; -/** @experimental */ class InformAboutCartRecalculation { public function __construct(private string $promotionName) diff --git a/src/Sylius/Bundle/ApiBundle/Command/Cart/PickupCart.php b/src/Sylius/Bundle/ApiBundle/Command/Cart/PickupCart.php index e5f6aa6a01..333f8da721 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Cart/PickupCart.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Cart/PickupCart.php @@ -17,16 +17,8 @@ use Sylius\Bundle\ApiBundle\Command\ChannelCodeAwareInterface; use Sylius\Bundle\ApiBundle\Command\CustomerEmailAwareInterface; use Sylius\Bundle\ApiBundle\Command\LocaleCodeAwareInterface; -/** @experimental */ class PickupCart implements ChannelCodeAwareInterface, CustomerEmailAwareInterface, LocaleCodeAwareInterface { - /** - * @immutable - * - * @var string|null - */ - public $tokenValue; - /** @var string|null */ public $localeCode; @@ -36,9 +28,8 @@ class PickupCart implements ChannelCodeAwareInterface, CustomerEmailAwareInterfa /** @var string|null */ public $email; - public function __construct(?string $tokenValue = null) + public function __construct(public ?string $tokenValue = null) { - $this->tokenValue = $tokenValue; } public function getChannelCode(): ?string diff --git a/src/Sylius/Bundle/ApiBundle/Command/Cart/RemoveItemFromCart.php b/src/Sylius/Bundle/ApiBundle/Command/Cart/RemoveItemFromCart.php index 19d82a7568..2639bedd90 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Cart/RemoveItemFromCart.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Cart/RemoveItemFromCart.php @@ -15,23 +15,10 @@ namespace Sylius\Bundle\ApiBundle\Command\Cart; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; -/** @experimental */ class RemoveItemFromCart implements OrderTokenValueAwareInterface { - /** @var string|null */ - public $orderTokenValue; - - /** - * @immutable - * - * @var string - */ - public $itemId; - - public function __construct(?string $orderTokenValue, string $itemId) + public function __construct(public ?string $orderTokenValue, public string $itemId) { - $this->orderTokenValue = $orderTokenValue; - $this->itemId = $itemId; } public static function removeFromData(string $tokenValue, string $orderItemId): self diff --git a/src/Sylius/Bundle/ApiBundle/Command/Catalog/AddProductReview.php b/src/Sylius/Bundle/ApiBundle/Command/Catalog/AddProductReview.php index 0cf2435ce2..0d6464961c 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Catalog/AddProductReview.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Catalog/AddProductReview.php @@ -16,56 +16,15 @@ namespace Sylius\Bundle\ApiBundle\Command\Catalog; use Sylius\Bundle\ApiBundle\Command\CustomerEmailAwareInterface; use Sylius\Bundle\ApiBundle\Command\IriToIdentifierConversionAwareInterface; -/** @experimental */ class AddProductReview implements IriToIdentifierConversionAwareInterface, CustomerEmailAwareInterface { - /** - * @immutable - * - * @var string|null - */ - public $title; - - /** - * @immutable - * - * @var int|null - */ - public $rating; - - /** - * @immutable - * - * @var string|null - */ - public $comment; - - /** - * @immutable - * - * @var string - */ - public $productCode; - - /** - * @immutable - * - * @var string|null - */ - public $email; - public function __construct( - ?string $title, - ?int $rating, - ?string $comment, - string $productCode, - ?string $email = null, + public string $title, + public int $rating, + public string $comment, + public string $productCode, + public ?string $email = null, ) { - $this->title = $title; - $this->rating = $rating; - $this->comment = $comment; - $this->productCode = $productCode; - $this->email = $email; } public function getEmail(): ?string diff --git a/src/Sylius/Bundle/ApiBundle/Command/ChannelCodeAwareInterface.php b/src/Sylius/Bundle/ApiBundle/Command/ChannelCodeAwareInterface.php index 16e2a284da..6649de5404 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/ChannelCodeAwareInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Command/ChannelCodeAwareInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command; -/** @experimental */ interface ChannelCodeAwareInterface extends CommandAwareDataTransformerInterface { public function getChannelCode(): ?string; diff --git a/src/Sylius/Bundle/ApiBundle/Command/Checkout/ChoosePaymentMethod.php b/src/Sylius/Bundle/ApiBundle/Command/Checkout/ChoosePaymentMethod.php index bf6b52af17..fbb45e951f 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Checkout/ChoosePaymentMethod.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Checkout/ChoosePaymentMethod.php @@ -18,7 +18,6 @@ use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; use Sylius\Bundle\ApiBundle\Command\PaymentMethodCodeAwareInterface; use Sylius\Bundle\ApiBundle\Command\SubresourceIdAwareInterface; -/** @experimental */ class ChoosePaymentMethod implements OrderTokenValueAwareInterface, SubresourceIdAwareInterface, PaymentMethodCodeAwareInterface, IriToIdentifierConversionAwareInterface { /** @var string|null */ diff --git a/src/Sylius/Bundle/ApiBundle/Command/Checkout/ChooseShippingMethod.php b/src/Sylius/Bundle/ApiBundle/Command/Checkout/ChooseShippingMethod.php index 85655ab00d..9b4f256b25 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Checkout/ChooseShippingMethod.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Checkout/ChooseShippingMethod.php @@ -17,7 +17,6 @@ use Sylius\Bundle\ApiBundle\Command\IriToIdentifierConversionAwareInterface; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; use Sylius\Bundle\ApiBundle\Command\SubresourceIdAwareInterface; -/** @experimental */ class ChooseShippingMethod implements OrderTokenValueAwareInterface, SubresourceIdAwareInterface, IriToIdentifierConversionAwareInterface { /** @var string|null */ diff --git a/src/Sylius/Bundle/ApiBundle/Command/Checkout/CompleteOrder.php b/src/Sylius/Bundle/ApiBundle/Command/Checkout/CompleteOrder.php index 4ee7146a71..03a823a78c 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Checkout/CompleteOrder.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Checkout/CompleteOrder.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Command\Checkout; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; -/** @experimental */ class CompleteOrder implements OrderTokenValueAwareInterface { /** @var string|null */ diff --git a/src/Sylius/Bundle/ApiBundle/Command/Checkout/SendOrderConfirmation.php b/src/Sylius/Bundle/ApiBundle/Command/Checkout/SendOrderConfirmation.php index e162b679d5..4e77264111 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Checkout/SendOrderConfirmation.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Checkout/SendOrderConfirmation.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command\Checkout; -/** @experimental */ class SendOrderConfirmation { /** @var string */ diff --git a/src/Sylius/Bundle/ApiBundle/Command/Checkout/SendShipmentConfirmationEmail.php b/src/Sylius/Bundle/ApiBundle/Command/Checkout/SendShipmentConfirmationEmail.php index ca6cef853e..57c3e91bac 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Checkout/SendShipmentConfirmationEmail.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Checkout/SendShipmentConfirmationEmail.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command\Checkout; -/** @experimental */ class SendShipmentConfirmationEmail { /** @var mixed */ diff --git a/src/Sylius/Bundle/ApiBundle/Command/Checkout/ShipShipment.php b/src/Sylius/Bundle/ApiBundle/Command/Checkout/ShipShipment.php index 6a0f3657a3..8c6928c4d5 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Checkout/ShipShipment.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Checkout/ShipShipment.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Command\Checkout; use Sylius\Bundle\ApiBundle\Command\ShipmentIdAwareInterface; -/** @experimental */ class ShipShipment implements ShipmentIdAwareInterface { /** @var int|null */ diff --git a/src/Sylius/Bundle/ApiBundle/Command/Checkout/UpdateCart.php b/src/Sylius/Bundle/ApiBundle/Command/Checkout/UpdateCart.php index bd5a29a71d..68a8ba0483 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/Checkout/UpdateCart.php +++ b/src/Sylius/Bundle/ApiBundle/Command/Checkout/UpdateCart.php @@ -18,7 +18,6 @@ use Sylius\Bundle\ApiBundle\Command\LocaleCodeAwareInterface; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; use Sylius\Component\Addressing\Model\AddressInterface; -/** @experimental */ class UpdateCart implements OrderTokenValueAwareInterface, CustomerEmailAwareInterface, LocaleCodeAwareInterface { public ?string $orderTokenValue = null; diff --git a/src/Sylius/Bundle/ApiBundle/Command/CommandAwareDataTransformerInterface.php b/src/Sylius/Bundle/ApiBundle/Command/CommandAwareDataTransformerInterface.php index bd76ad8c81..d41867b1e6 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/CommandAwareDataTransformerInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Command/CommandAwareDataTransformerInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command; -/** @experimental */ interface CommandAwareDataTransformerInterface { } diff --git a/src/Sylius/Bundle/ApiBundle/Command/Customer/RemoveShopUser.php b/src/Sylius/Bundle/ApiBundle/Command/Customer/RemoveShopUser.php new file mode 100644 index 0000000000..8cc3160631 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Command/Customer/RemoveShopUser.php @@ -0,0 +1,33 @@ +shopUserId; + } + + public function setShopUserId(mixed $shopUserId): void + { + $this->shopUserId = $shopUserId; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Command/CustomerEmailAwareInterface.php b/src/Sylius/Bundle/ApiBundle/Command/CustomerEmailAwareInterface.php index 1abb531914..1fe31dc631 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/CustomerEmailAwareInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Command/CustomerEmailAwareInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command; -/** @experimental */ interface CustomerEmailAwareInterface extends CommandAwareDataTransformerInterface { public function getEmail(): ?string; diff --git a/src/Sylius/Bundle/ApiBundle/Command/LocaleCodeAwareInterface.php b/src/Sylius/Bundle/ApiBundle/Command/LocaleCodeAwareInterface.php index 7cf3d399ea..83bad10d8e 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/LocaleCodeAwareInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Command/LocaleCodeAwareInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command; -/** @experimental */ interface LocaleCodeAwareInterface extends CommandAwareDataTransformerInterface { public function getLocaleCode(): ?string; diff --git a/src/Sylius/Bundle/ApiBundle/Command/LoggedInCustomerEmailIfNotSetAwareInterface.php b/src/Sylius/Bundle/ApiBundle/Command/LoggedInCustomerEmailIfNotSetAwareInterface.php index 727205cc52..a06af4c196 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/LoggedInCustomerEmailIfNotSetAwareInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Command/LoggedInCustomerEmailIfNotSetAwareInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command; -/** @experimental */ interface LoggedInCustomerEmailIfNotSetAwareInterface extends CommandAwareDataTransformerInterface { public function getEmail(): ?string; diff --git a/src/Sylius/Bundle/ApiBundle/Command/OrderTokenValueAwareInterface.php b/src/Sylius/Bundle/ApiBundle/Command/OrderTokenValueAwareInterface.php index f7fd0f837b..c00800949b 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/OrderTokenValueAwareInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Command/OrderTokenValueAwareInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command; -/** @experimental */ interface OrderTokenValueAwareInterface extends CommandAwareDataTransformerInterface { public function getOrderTokenValue(): ?string; diff --git a/src/Sylius/Bundle/ApiBundle/Command/PaymentMethodCodeAwareInterface.php b/src/Sylius/Bundle/ApiBundle/Command/PaymentMethodCodeAwareInterface.php index 55c234ce55..8e73ab1b05 100644 --- a/src/Sylius/Bundle/ApiBundle/Command/PaymentMethodCodeAwareInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Command/PaymentMethodCodeAwareInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Command; -/** @experimental */ interface PaymentMethodCodeAwareInterface extends CommandAwareDataTransformerInterface { public function getPaymentMethodCode(): ?string; diff --git a/src/Sylius/Bundle/ApiBundle/Command/Promotion/GeneratePromotionCoupon.php b/src/Sylius/Bundle/ApiBundle/Command/Promotion/GeneratePromotionCoupon.php new file mode 100644 index 0000000000..ba2854c150 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Command/Promotion/GeneratePromotionCoupon.php @@ -0,0 +1,65 @@ +promotionCode; + } + + public function getAmount(): int + { + return $this->amount; + } + + public function getCodeLength(): int + { + return $this->codeLength; + } + + public function getPrefix(): ?string + { + return $this->prefix; + } + + public function getSuffix(): ?string + { + return $this->suffix; + } + + public function getExpiresAt(): ?\DateTimeInterface + { + return $this->expiresAt; + } + + public function getUsageLimit(): ?int + { + return $this->usageLimit; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Command/ResendOrderConfirmationEmail.php b/src/Sylius/Bundle/ApiBundle/Command/ResendOrderConfirmationEmail.php new file mode 100644 index 0000000000..6582fd285c --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Command/ResendOrderConfirmationEmail.php @@ -0,0 +1,20 @@ + $shopUserRepository + */ public function __construct( private UserRepositoryInterface $shopUserRepository, private GeneratorInterface $tokenGenerator, diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/ResetPasswordHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/ResetPasswordHandler.php index 1c53e8508e..caacc69076 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/ResetPasswordHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/ResetPasswordHandler.php @@ -17,7 +17,6 @@ use Sylius\Bundle\ApiBundle\Command\Account\ResetPassword; use Sylius\Bundle\CoreBundle\Security\UserPasswordResetterInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; -/** @experimental */ final class ResetPasswordHandler implements MessageHandlerInterface { public function __construct(private UserPasswordResetterInterface $userPasswordResetter) diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountRegistrationEmailHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountRegistrationEmailHandler.php index c0c5bbf846..bf33b969d7 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountRegistrationEmailHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountRegistrationEmailHandler.php @@ -14,20 +14,19 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler\Account; use Sylius\Bundle\ApiBundle\Command\Account\SendAccountRegistrationEmail; -use Sylius\Bundle\CoreBundle\Mailer\Emails; +use Sylius\Bundle\CoreBundle\Mailer\AccountRegistrationEmailManagerInterface; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; +use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ShopUserInterface; -use Sylius\Component\Mailer\Sender\SenderInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; -/** @experimental */ final class SendAccountRegistrationEmailHandler implements MessageHandlerInterface { public function __construct( private UserRepositoryInterface $shopUserRepository, private ChannelRepositoryInterface $channelRepository, - private SenderInterface $emailSender, + private AccountRegistrationEmailManagerInterface $accountRegistrationEmailManager, ) { } @@ -36,12 +35,13 @@ final class SendAccountRegistrationEmailHandler implements MessageHandlerInterfa /** @var ShopUserInterface $shopUser */ $shopUser = $this->shopUserRepository->findOneByEmail($command->shopUserEmail); + /** @var ChannelInterface $channel */ $channel = $this->channelRepository->findOneByCode($command->channelCode); - $this->emailSender->send( - Emails::USER_REGISTRATION, - [$command->shopUserEmail], - ['user' => $shopUser, 'localeCode' => $command->localeCode, 'channel' => $channel], - ); + if ($channel->isAccountVerificationRequired() && !$shopUser->isEnabled()) { + return; + } + + $this->accountRegistrationEmailManager->sendAccountRegistrationEmail($shopUser, $channel, $command->localeCode); } } diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountVerificationEmailHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountVerificationEmailHandler.php index 6f2a8eee39..bd1febe528 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountVerificationEmailHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendAccountVerificationEmailHandler.php @@ -14,34 +14,48 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler\Account; use Sylius\Bundle\ApiBundle\Command\Account\SendAccountVerificationEmail; -use Sylius\Bundle\CoreBundle\Mailer\Emails; +use Sylius\Bundle\ApiBundle\Exception\ChannelNotFoundException; +use Sylius\Bundle\ApiBundle\Exception\UserNotFoundException; +use Sylius\Bundle\CoreBundle\Mailer\AccountVerificationEmailManagerInterface; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; +use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ShopUserInterface; -use Sylius\Component\Mailer\Sender\SenderInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; -/** @experimental */ final class SendAccountVerificationEmailHandler implements MessageHandlerInterface { + /** + * @param UserRepositoryInterface $shopUserRepository + * @param ChannelRepositoryInterface $channelRepository + */ public function __construct( private UserRepositoryInterface $shopUserRepository, private ChannelRepositoryInterface $channelRepository, - private SenderInterface $emailSender, + private AccountVerificationEmailManagerInterface $accountVerificationEmailManager, ) { } public function __invoke(SendAccountVerificationEmail $command): void { - /** @var ShopUserInterface $shopUser */ $shopUser = $this->shopUserRepository->findOneByEmail($command->shopUserEmail); + if ($shopUser === null) { + throw new UserNotFoundException(sprintf('User with email %s has not been found.', $command->shopUserEmail)); + } + $channel = $this->channelRepository->findOneByCode($command->channelCode); - $this->emailSender->send( - Emails::ACCOUNT_VERIFICATION_TOKEN, - [$command->shopUserEmail], - ['user' => $shopUser, 'localeCode' => $command->localeCode, 'channel' => $channel], + if ($channel === null) { + throw new ChannelNotFoundException( + sprintf('Channel with code %s has not been found.', $command->channelCode), + ); + } + + $this->accountVerificationEmailManager->sendAccountVerificationEmail( + $shopUser, + $channel, + $command->localeCode, ); } } diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendResetPasswordEmailHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendResetPasswordEmailHandler.php index cdfd8c81d8..23b2a7db4c 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendResetPasswordEmailHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/SendResetPasswordEmailHandler.php @@ -14,35 +14,31 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler\Account; use Sylius\Bundle\ApiBundle\Command\Account\SendResetPasswordEmail; -use Sylius\Bundle\CoreBundle\Mailer\Emails; +use Sylius\Bundle\CoreBundle\Mailer\ResetPasswordEmailManagerInterface; +use Sylius\Component\Channel\Model\ChannelInterface; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; -use Sylius\Component\Mailer\Sender\SenderInterface; +use Sylius\Component\User\Model\UserInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; -/** @experimental */ final class SendResetPasswordEmailHandler implements MessageHandlerInterface { + /** + * @param ChannelRepositoryInterface $channelRepository + * @param UserRepositoryInterface $userRepository + */ public function __construct( - private SenderInterface $emailSender, private ChannelRepositoryInterface $channelRepository, private UserRepositoryInterface $userRepository, + private ResetPasswordEmailManagerInterface $resetPasswordEmailManager, ) { } - public function __invoke(SendResetPasswordEmail $command) + public function __invoke(SendResetPasswordEmail $command): void { $user = $this->userRepository->findOneByEmail($command->email); $channel = $this->channelRepository->findOneByCode($command->channelCode()); - $this->emailSender->send( - Emails::PASSWORD_RESET, - [$command->email], - [ - 'user' => $user, - 'localeCode' => $command->localeCode(), - 'channel' => $channel, - ], - ); + $this->resetPasswordEmailManager->sendResetPasswordEmail($user, $channel, $command->localeCode()); } } diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/VerifyCustomerAccountHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/VerifyCustomerAccountHandler.php index 55d81c382d..6e443c9ff9 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/VerifyCustomerAccountHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Account/VerifyCustomerAccountHandler.php @@ -14,29 +14,31 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler\Account; use InvalidArgumentException; +use Sylius\Bundle\ApiBundle\Command\Account\SendAccountRegistrationEmail; use Sylius\Bundle\ApiBundle\Command\Account\VerifyCustomerAccount; use Sylius\Calendar\Provider\DateTimeProviderInterface; +use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; -use Sylius\Component\User\Model\UserInterface; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; -/** @experimental */ final class VerifyCustomerAccountHandler implements MessageHandlerInterface { public function __construct( private RepositoryInterface $shopUserRepository, private DateTimeProviderInterface $calendar, + private MessageBusInterface $commandBus, ) { } - public function __invoke(VerifyCustomerAccount $command): JsonResponse + public function __invoke(VerifyCustomerAccount $command): void { - /** @var UserInterface|null $user */ + /** @var ShopUserInterface|null $user */ $user = $this->shopUserRepository->findOneBy(['emailVerificationToken' => $command->token]); if (null === $user) { throw new InvalidArgumentException( - sprintf('There is no shop user with %s email verification token', $command->token), + sprintf('There is no shop user with "%s" email verification token', $command->token), ); } @@ -44,6 +46,9 @@ final class VerifyCustomerAccountHandler implements MessageHandlerInterface $user->setEmailVerificationToken(null); $user->enable(); - return new JsonResponse([]); + $this->commandBus->dispatch( + new SendAccountRegistrationEmail($user->getEmail(), $command->getLocaleCode(), $command->getChannelCode()), + [new DispatchAfterCurrentBusStamp()], + ); } } diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/AddItemToCartHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/AddItemToCartHandler.php index 50dd466383..c8854920f2 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/AddItemToCartHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/AddItemToCartHandler.php @@ -25,7 +25,6 @@ use Sylius\Component\Order\Modifier\OrderModifierInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class AddItemToCartHandler implements MessageHandlerInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/ChangeItemQuantityInCartHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/ChangeItemQuantityInCartHandler.php index 6aabf1111a..3700454a57 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/ChangeItemQuantityInCartHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/ChangeItemQuantityInCartHandler.php @@ -22,7 +22,6 @@ use Sylius\Component\Order\Repository\OrderItemRepositoryInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class ChangeItemQuantityInCartHandler implements MessageHandlerInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/InformAboutCartRecalculationHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/InformAboutCartRecalculationHandler.php index 0c625e7b97..126c8e709d 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/InformAboutCartRecalculationHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/InformAboutCartRecalculationHandler.php @@ -17,7 +17,6 @@ use Sylius\Bundle\ApiBundle\Command\Cart\InformAboutCartRecalculation; use Sylius\Bundle\ApiBundle\Exception\OrderNoLongerEligibleForPromotion; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; -/** @experimental */ final class InformAboutCartRecalculationHandler implements MessageHandlerInterface { public function __invoke(InformAboutCartRecalculation $command): void diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/PickupCartHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/PickupCartHandler.php index aa43cda57a..8e6263169a 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/PickupCartHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/PickupCartHandler.php @@ -27,7 +27,6 @@ use Sylius\Component\Resource\Generator\RandomnessGeneratorInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class PickupCartHandler implements MessageHandlerInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/RemoveItemFromCartHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/RemoveItemFromCartHandler.php index 6da65464fa..f10d05ba3e 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/RemoveItemFromCartHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Cart/RemoveItemFromCartHandler.php @@ -21,7 +21,6 @@ use Sylius\Component\Order\Repository\OrderItemRepositoryInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class RemoveItemFromCartHandler implements MessageHandlerInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Catalog/AddProductReviewHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Catalog/AddProductReviewHandler.php index 67f588b903..1ec5c4b949 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Catalog/AddProductReviewHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Catalog/AddProductReviewHandler.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler\Catalog; use Sylius\Bundle\ApiBundle\Command\Catalog\AddProductReview; +use Sylius\Bundle\ApiBundle\Exception\ProductNotFoundException; use Sylius\Bundle\CoreBundle\Resolver\CustomerResolverInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ProductInterface; @@ -21,9 +22,9 @@ use Sylius\Component\Core\Repository\ProductRepositoryInterface; use Sylius\Component\Resource\Factory\FactoryInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Component\Review\Model\ReviewInterface; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; -/** @experimental */ final class AddProductReviewHandler implements MessageHandlerInterface { public function __construct( @@ -36,9 +37,13 @@ final class AddProductReviewHandler implements MessageHandlerInterface public function __invoke(AddProductReview $addProductReview): ReviewInterface { - /** @var ProductInterface $product */ + /** @var ProductInterface|null $product */ $product = $this->productRepository->findOneByCode($addProductReview->productCode); + if ($product === null) { + throw new ProductNotFoundException(Response::HTTP_UNPROCESSABLE_ENTITY); + } + /** @var string|null $email */ $email = $addProductReview->getEmail(); diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChoosePaymentMethodHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChoosePaymentMethodHandler.php index c2933e67da..1585d4220b 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChoosePaymentMethodHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChoosePaymentMethodHandler.php @@ -14,8 +14,11 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler\Checkout; use SM\Factory\FactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Bundle\ApiBundle\Changer\PaymentMethodChangerInterface; use Sylius\Bundle\ApiBundle\Command\Checkout\ChoosePaymentMethod; +use Sylius\Bundle\ApiBundle\Exception\PaymentMethodCannotBeChangedException; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\Component\Core\OrderCheckoutTransitions; @@ -25,16 +28,26 @@ use Sylius\Component\Core\Repository\PaymentRepositoryInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class ChoosePaymentMethodHandler implements MessageHandlerInterface { public function __construct( private OrderRepositoryInterface $orderRepository, private PaymentMethodRepositoryInterface $paymentMethodRepository, private PaymentRepositoryInterface $paymentRepository, - private FactoryInterface $stateMachineFactory, + private FactoryInterface|StateMachineInterface $stateMachineFactory, private PaymentMethodChangerInterface $paymentMethodChanger, ) { + if ($this->stateMachineFactory instanceof FactoryInterface) { + trigger_deprecation( + 'sylius/api-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the fourth argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + FactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function __invoke(ChoosePaymentMethod $choosePaymentMethod): OrderInterface @@ -63,21 +76,27 @@ final class ChoosePaymentMethodHandler implements MessageHandlerInterface Assert::notNull($payment, 'Can not find payment with given identifier.'); if ($cart->getState() === OrderInterface::STATE_CART) { - $stateMachine = $this->stateMachineFactory->get($cart, OrderCheckoutTransitions::GRAPH); - + $stateMachine = $this->getStateMachine(); Assert::true( - $stateMachine->can( - OrderCheckoutTransitions::TRANSITION_SELECT_PAYMENT, - ), + $stateMachine->can($cart, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_SELECT_PAYMENT), 'Order cannot have payment method assigned.', ); $payment->setMethod($paymentMethod); - $stateMachine->apply(OrderCheckoutTransitions::TRANSITION_SELECT_PAYMENT); + $stateMachine->apply($cart, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_SELECT_PAYMENT); return $cart; } - throw new \InvalidArgumentException('Payment method can not be set'); + throw new PaymentMethodCannotBeChangedException(); + } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof FactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; } } diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChooseShippingMethodHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChooseShippingMethodHandler.php index 264f5f1c88..4173371c0d 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChooseShippingMethodHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ChooseShippingMethodHandler.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler\Checkout; use SM\Factory\FactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Bundle\ApiBundle\Command\Checkout\ChooseShippingMethod; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\ShippingMethodInterface; @@ -25,7 +27,6 @@ use Sylius\Component\Shipping\Checker\Eligibility\ShippingMethodEligibilityCheck use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class ChooseShippingMethodHandler implements MessageHandlerInterface { public function __construct( @@ -33,8 +34,19 @@ final class ChooseShippingMethodHandler implements MessageHandlerInterface private ShippingMethodRepositoryInterface $shippingMethodRepository, private ShipmentRepositoryInterface $shipmentRepository, private ShippingMethodEligibilityCheckerInterface $eligibilityChecker, - private FactoryInterface $stateMachineFactory, + private FactoryInterface|StateMachineInterface $stateMachineFactory, ) { + if ($this->stateMachineFactory instanceof FactoryInterface) { + trigger_deprecation( + 'sylius/api-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the fifth argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + FactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function __invoke(ChooseShippingMethod $chooseShippingMethod): OrderInterface @@ -44,10 +56,9 @@ final class ChooseShippingMethodHandler implements MessageHandlerInterface Assert::notNull($cart, 'Cart has not been found.'); - $stateMachine = $this->stateMachineFactory->get($cart, OrderCheckoutTransitions::GRAPH); - + $stateMachine = $this->getStateMachine(); Assert::true( - $stateMachine->can(OrderCheckoutTransitions::TRANSITION_SELECT_SHIPPING), + $stateMachine->can($cart, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_SELECT_SHIPPING), 'Order cannot have shipment method assigned.', ); @@ -66,8 +77,17 @@ final class ChooseShippingMethodHandler implements MessageHandlerInterface ); $shipment->setMethod($shippingMethod); - $stateMachine->apply(OrderCheckoutTransitions::TRANSITION_SELECT_SHIPPING); + $stateMachine->apply($cart, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_SELECT_SHIPPING); return $cart; } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof FactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } } diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/CompleteOrderHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/CompleteOrderHandler.php index 916087c768..f5d6c46d6f 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/CompleteOrderHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/CompleteOrderHandler.php @@ -14,8 +14,12 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler\Checkout; use SM\Factory\FactoryInterface; +use Sylius\Abstraction\StateMachine\Exception\StateMachineExecutionException; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Bundle\ApiBundle\Command\Cart\InformAboutCartRecalculation; use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder; +use Sylius\Bundle\ApiBundle\CommandHandler\Checkout\Exception\OrderTotalHasChangedException; use Sylius\Bundle\ApiBundle\Event\OrderCompleted; use Sylius\Bundle\CoreBundle\Order\Checker\OrderPromotionsIntegrityCheckerInterface; use Sylius\Component\Core\Model\OrderInterface; @@ -26,18 +30,32 @@ use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; use Webmozart\Assert\Assert; -/** @experimental */ final class CompleteOrderHandler implements MessageHandlerInterface { public function __construct( private OrderRepositoryInterface $orderRepository, - private FactoryInterface $stateMachineFactory, + private FactoryInterface|StateMachineInterface $stateMachineFactory, private MessageBusInterface $commandBus, private MessageBusInterface $eventBus, private OrderPromotionsIntegrityCheckerInterface $orderPromotionsIntegrityChecker, ) { + if ($this->stateMachineFactory instanceof FactoryInterface) { + trigger_deprecation( + 'sylius/api-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the second argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + FactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } + /** + * @throws StateMachineExecutionException + * @throws OrderTotalHasChangedException + */ public function __invoke(CompleteOrder $completeOrder): OrderInterface { $orderTokenValue = $completeOrder->orderTokenValue; @@ -52,6 +70,8 @@ final class CompleteOrderHandler implements MessageHandlerInterface $cart->setNotes($completeOrder->notes); } + $oldTotal = $cart->getTotal(); + if ($promotion = $this->orderPromotionsIntegrityChecker->check($cart)) { $this->commandBus->dispatch( new InformAboutCartRecalculation($promotion->getName()), @@ -61,17 +81,29 @@ final class CompleteOrderHandler implements MessageHandlerInterface return $cart; } - $stateMachine = $this->stateMachineFactory->get($cart, OrderCheckoutTransitions::GRAPH); + if ($oldTotal !== $cart->getTotal()) { + throw new OrderTotalHasChangedException(); + } + $stateMachine = $this->getStateMachine(); Assert::true( - $stateMachine->can(OrderCheckoutTransitions::TRANSITION_COMPLETE), + $stateMachine->can($cart, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_COMPLETE), sprintf('Order with %s token cannot be completed.', $orderTokenValue), ); - $stateMachine->apply(OrderCheckoutTransitions::TRANSITION_COMPLETE); + $stateMachine->apply($cart, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_COMPLETE); $this->eventBus->dispatch(new OrderCompleted($cart->getTokenValue()), [new DispatchAfterCurrentBusStamp()]); return $cart; } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof FactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } } diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/Exception/OrderTotalHasChangedException.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/Exception/OrderTotalHasChangedException.php new file mode 100644 index 0000000000..338a2ea079 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/Exception/OrderTotalHasChangedException.php @@ -0,0 +1,28 @@ +getCustomer()->getEmail(); Assert::notNull($email); - $this->emailSender->send( - Emails::ORDER_CONFIRMATION, - [$email], - [ - 'order' => $order, - 'channel' => $order->getChannel(), - 'localeCode' => $order->getLocaleCode(), - ], - ); + $this->orderEmailManager->sendConfirmationEmail($order); } } diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/SendShipmentConfirmationEmailHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/SendShipmentConfirmationEmailHandler.php index 25ed808cad..3f9e2083de 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/SendShipmentConfirmationEmailHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/SendShipmentConfirmationEmailHandler.php @@ -14,19 +14,17 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler\Checkout; use Sylius\Bundle\ApiBundle\Command\Checkout\SendShipmentConfirmationEmail; -use Sylius\Bundle\CoreBundle\Mailer\Emails; +use Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface; use Sylius\Component\Core\Model\ShipmentInterface; use Sylius\Component\Core\Repository\ShipmentRepositoryInterface; -use Sylius\Component\Mailer\Sender\SenderInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class SendShipmentConfirmationEmailHandler implements MessageHandlerInterface { public function __construct( - private SenderInterface $emailSender, private ShipmentRepositoryInterface $shipmentRepository, + private ShipmentEmailManagerInterface $shipmentEmailManager, ) { } @@ -40,15 +38,6 @@ final class SendShipmentConfirmationEmailHandler implements MessageHandlerInterf $email = $order->getCustomer()->getEmail(); Assert::notNull($email); - $this->emailSender->send( - Emails::SHIPMENT_CONFIRMATION, - [$email], - [ - 'shipment' => $shipment, - 'order' => $order, - 'channel' => $order->getChannel(), - 'localeCode' => $order->getLocaleCode(), - ], - ); + $this->shipmentEmailManager->sendConfirmationEmail($shipment); } } diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ShipShipmentHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ShipShipmentHandler.php index 7cd2ec9a7a..f65997e824 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ShipShipmentHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/ShipShipmentHandler.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler\Checkout; use SM\Factory\FactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Bundle\ApiBundle\Command\Checkout\SendShipmentConfirmationEmail; use Sylius\Bundle\ApiBundle\Command\Checkout\ShipShipment; use Sylius\Component\Core\Model\ShipmentInterface; @@ -24,14 +26,24 @@ use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; use Webmozart\Assert\Assert; -/** @experimental */ final class ShipShipmentHandler implements MessageHandlerInterface { public function __construct( private ShipmentRepositoryInterface $shipmentRepository, - private FactoryInterface $stateMachineFactory, + private FactoryInterface|StateMachineInterface $stateMachineFactory, private MessageBusInterface $eventBus, ) { + if ($this->stateMachineFactory instanceof FactoryInterface) { + trigger_deprecation( + 'sylius/api-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the second argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + FactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function __invoke(ShipShipment $shipShipment): ShipmentInterface @@ -45,17 +57,25 @@ final class ShipShipmentHandler implements MessageHandlerInterface $shipment->setTracking($shipShipment->trackingCode); } - $stateMachine = $this->stateMachineFactory->get($shipment, ShipmentTransitions::GRAPH); - + $stateMachine = $this->getStateMachine(); Assert::true( - $stateMachine->can(ShipmentTransitions::TRANSITION_SHIP), + $stateMachine->can($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_SHIP), 'This shipment cannot be completed.', ); - $stateMachine->apply(ShipmentTransitions::TRANSITION_SHIP); + $stateMachine->apply($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_SHIP); $this->eventBus->dispatch(new SendShipmentConfirmationEmail($shipShipment->shipmentId), [new DispatchAfterCurrentBusStamp()]); return $shipment; } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof FactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } } diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/UpdateCartHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/UpdateCartHandler.php index 1802e34001..eb9056baef 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/UpdateCartHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Checkout/UpdateCartHandler.php @@ -23,7 +23,6 @@ use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class UpdateCartHandler implements MessageHandlerInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Customer/RemoveShopUserHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Customer/RemoveShopUserHandler.php new file mode 100644 index 0000000000..8f97c8255c --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Customer/RemoveShopUserHandler.php @@ -0,0 +1,42 @@ + $shopUserRepository + */ + public function __construct( + private UserRepositoryInterface $shopUserRepository, + ) { + } + + public function __invoke(RemoveShopUser $removeShopUser): void + { + $shopUser = $this->shopUserRepository->find($removeShopUser->getShopUserId()); + + if (null === $shopUser) { + throw new UserNotFoundException(); + } + + $shopUser->setCustomer(null); + $this->shopUserRepository->remove($shopUser); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/Promotion/GeneratePromotionCouponHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/Promotion/GeneratePromotionCouponHandler.php new file mode 100644 index 0000000000..88c130ad20 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/Promotion/GeneratePromotionCouponHandler.php @@ -0,0 +1,53 @@ + $promotionRepository + */ + public function __construct( + private PromotionRepositoryInterface $promotionRepository, + private PromotionCouponGeneratorInterface $promotionCouponGenerator, + ) { + } + + /** @return Collection */ + public function __invoke(GeneratePromotionCoupon $generatePromotionCoupon): Collection + { + /** @var PromotionInterface|null $promotion */ + $promotion = $this->promotionRepository->findOneBy(['code' => $generatePromotionCoupon->getPromotionCode()]); + if ($promotion === null) { + throw new PromotionNotFoundException($generatePromotionCoupon->getPromotionCode()); + } + + $promotionCoupons = $this->promotionCouponGenerator->generate( + $promotion, + $generatePromotionCoupon, + ); + + return new ArrayCollection($promotionCoupons); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/CommandHandler/SendContactRequestHandler.php b/src/Sylius/Bundle/ApiBundle/CommandHandler/SendContactRequestHandler.php index 3408dfe23b..043cb56d89 100644 --- a/src/Sylius/Bundle/ApiBundle/CommandHandler/SendContactRequestHandler.php +++ b/src/Sylius/Bundle/ApiBundle/CommandHandler/SendContactRequestHandler.php @@ -14,41 +14,44 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\CommandHandler; use Sylius\Bundle\ApiBundle\Command\SendContactRequest; -use Sylius\Bundle\CoreBundle\Mailer\Emails; +use Sylius\Bundle\ApiBundle\Exception\ChannelNotFoundException; +use Sylius\Bundle\CoreBundle\Mailer\ContactEmailManagerInterface; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; use Sylius\Component\Core\Model\ChannelInterface; -use Sylius\Component\Mailer\Sender\SenderInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Webmozart\Assert\Assert; -/** experimental */ final class SendContactRequestHandler implements MessageHandlerInterface { + /** + * @param ChannelRepositoryInterface $channelRepository + */ public function __construct( - private SenderInterface $sender, private ChannelRepositoryInterface $channelRepository, + private ContactEmailManagerInterface $contactEmailManager, ) { } public function __invoke(SendContactRequest $command): void { - /** @var ChannelInterface $channel */ + /** @var ChannelInterface|null $channel */ $channel = $this->channelRepository->findOneByCode($command->getChannelCode()); - Assert::notNull($channel); + + if ($channel === null) { + throw new ChannelNotFoundException($command->getChannelCode()); + } $email = $command->getEmail(); Assert::notNull($email); - $this->sender->send( - Emails::CONTACT_REQUEST, - [$channel->getContactEmail()], + $this->contactEmailManager->sendContactRequest( [ - 'data' => ['message' => $command->getMessage(), 'email' => $email], - 'channel' => $channel, - 'localeCode' => $command->getLocaleCode(), + 'email' => $email, + 'message' => $command->getMessage(), ], - [], - [$email], + [$channel->getContactEmail()], + $channel, + $command->localeCode, ); } } diff --git a/src/Sylius/Bundle/ApiBundle/Context/TokenBasedUserContext.php b/src/Sylius/Bundle/ApiBundle/Context/TokenBasedUserContext.php index 806eb61540..4fc2cc7791 100644 --- a/src/Sylius/Bundle/ApiBundle/Context/TokenBasedUserContext.php +++ b/src/Sylius/Bundle/ApiBundle/Context/TokenBasedUserContext.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\Context; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\User\UserInterface; -/** @experimental */ final class TokenBasedUserContext implements UserContextInterface { public function __construct(private TokenStorageInterface $tokenStorage) diff --git a/src/Sylius/Bundle/ApiBundle/Context/TokenValueBasedCartContext.php b/src/Sylius/Bundle/ApiBundle/Context/TokenValueBasedCartContext.php index 0baad2d335..cc05d6ac5a 100644 --- a/src/Sylius/Bundle/ApiBundle/Context/TokenValueBasedCartContext.php +++ b/src/Sylius/Bundle/ApiBundle/Context/TokenValueBasedCartContext.php @@ -20,7 +20,6 @@ use Sylius\Component\Order\Model\OrderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; -/** @experimental */ final class TokenValueBasedCartContext implements CartContextInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/Context/UserContextInterface.php b/src/Sylius/Bundle/ApiBundle/Context/UserContextInterface.php index bd1477f224..9d4f3fe137 100644 --- a/src/Sylius/Bundle/ApiBundle/Context/UserContextInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Context/UserContextInterface.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Context; use Symfony\Component\Security\Core\User\UserInterface; -/** @experimental */ interface UserContextInterface { public function getUser(): ?UserInterface; diff --git a/src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryCollectionAction.php b/src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryCollectionAction.php new file mode 100644 index 0000000000..99848d193f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Controller/GetAddressLogEntryCollectionAction.php @@ -0,0 +1,40 @@ +messageBus = $messageBus; + } + + /** @return Collection */ + public function __invoke(int $id): Collection + { + return $this->handle( + $this->messageBus->dispatch( + new GetAddressLogEntryCollection($id), + ), + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Controller/GetCustomerStatisticsAction.php b/src/Sylius/Bundle/ApiBundle/Controller/GetCustomerStatisticsAction.php new file mode 100644 index 0000000000..2dfedb62d5 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Controller/GetCustomerStatisticsAction.php @@ -0,0 +1,38 @@ +messageBus = $messageBus; + } + + public function __invoke(int $id): CustomerStatistics + { + return $this->handle( + $this->messageBus->dispatch( + new GetCustomerStatistics($id), + ), + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Controller/GetOrderAdjustmentsAction.php b/src/Sylius/Bundle/ApiBundle/Controller/GetOrderAdjustmentsAction.php new file mode 100644 index 0000000000..2d13dc1707 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Controller/GetOrderAdjustmentsAction.php @@ -0,0 +1,43 @@ + $orderRepository + */ + public function __construct( + private OrderRepositoryInterface $orderRepository, + ) { + } + + /** + * @return Collection + */ + public function __invoke(Request $request, string $tokenValue): Collection + { + /** @var OrderInterface $order */ + $order = $this->orderRepository->findOneBy(['tokenValue' => $tokenValue]); + $type = $request->query->get('type'); + + return $order->getAdjustmentsRecursively($type); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Controller/GetProductBySlugAction.php b/src/Sylius/Bundle/ApiBundle/Controller/GetProductBySlugAction.php index ab791a4044..8548fea499 100644 --- a/src/Sylius/Bundle/ApiBundle/Controller/GetProductBySlugAction.php +++ b/src/Sylius/Bundle/ApiBundle/Controller/GetProductBySlugAction.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Controller; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Repository\ProductRepositoryInterface; @@ -47,7 +47,7 @@ final class GetProductBySlugAction throw new NotFoundHttpException('Not Found'); } - $iri = $this->iriConverter->getIriFromItem($product); + $iri = $this->iriConverter->getIriFromResource($product); $request = $this->requestStack->getCurrentRequest(); diff --git a/src/Sylius/Bundle/ApiBundle/Controller/GetStatisticsAction.php b/src/Sylius/Bundle/ApiBundle/Controller/GetStatisticsAction.php new file mode 100644 index 0000000000..28987ffe48 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Controller/GetStatisticsAction.php @@ -0,0 +1,123 @@ + */ + private array $constraints; + + /** @var array */ + private array $intervalsMap; + + /** @param array $intervalsMap */ + public function __construct( + MessageBusInterface $messageBus, + private SerializerInterface $serializer, + private ValidatorInterface $validator, + array $intervalsMap, + ) { + $this->messageBus = $messageBus; + $this->intervalsMap = $this->populateIntervals($intervalsMap); + $this->constraints = $this->createInputDataConstraints(); + } + + /** + * @throws \Throwable + */ + public function __invoke(Request $request): Response + { + $parameters = $request->query->all(); + + $violations = $this->validator->validate($parameters, $this->constraints); + if (count($violations) > 0) { + throw new ValidationException($violations); + } + + $interval = $parameters['interval']; + + $period = new \DatePeriod( + new \DateTimeImmutable($parameters['startDate']), + new \DateInterval($this->intervalsMap[$interval]), + new \DateTimeImmutable($parameters['endDate']), + ); + + try { + $result = $this->handle(new GetStatistics( + $interval, + $period, + $parameters['channelCode'], + )); + + return new JsonResponse( + data: $this->serializer->serialize($result, 'json'), + status: Response::HTTP_OK, + json: true, + ); + } catch (HandlerFailedException $exception) { + throw $exception->getPrevious(); + } + } + + /** @return array */ + private function createInputDataConstraints(): array + { + return [ + new SymfonyConstraints\Collection([ + 'channelCode' => new Constraints\Code(), + 'startDate' => [ + new SymfonyConstraints\NotBlank(), + new SymfonyConstraints\DateTime('Y-m-d\TH:i:s', message: 'sylius.date_time.invalid'), + ], + 'interval' => new SymfonyConstraints\Choice(choices: array_keys($this->intervalsMap), multiple: false), + 'endDate' => [ + new SymfonyConstraints\NotBlank(), + new SymfonyConstraints\DateTime('Y-m-d\TH:i:s', message: 'sylius.date_time.invalid'), + ], + ]), + new SymfonyConstraints\Expression(expression: 'value["startDate"] < value["endDate"]', message: 'sylius.statistics.end_date.invalid'), + ]; + } + + /** + * @param array $intervalsMap + * + * @return array + */ + private function populateIntervals(array $intervalsMap): array + { + $intervals = []; + foreach ($intervalsMap as $type => $intervalMap) { + $intervals[$type] = $intervalMap['interval']; + } + + return $intervals; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Controller/Payment/GetPaymentConfiguration.php b/src/Sylius/Bundle/ApiBundle/Controller/Payment/GetPaymentConfiguration.php index ea105bd90a..2537123fa1 100644 --- a/src/Sylius/Bundle/ApiBundle/Controller/Payment/GetPaymentConfiguration.php +++ b/src/Sylius/Bundle/ApiBundle/Controller/Payment/GetPaymentConfiguration.php @@ -20,7 +20,6 @@ use Sylius\Component\Core\Repository\PaymentRepositoryInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; -/** @experimental */ final class GetPaymentConfiguration { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/Controller/RemoveCatalogPromotionAction.php b/src/Sylius/Bundle/ApiBundle/Controller/RemoveCatalogPromotionAction.php index e402f9bf4e..3e55322cac 100644 --- a/src/Sylius/Bundle/ApiBundle/Controller/RemoveCatalogPromotionAction.php +++ b/src/Sylius/Bundle/ApiBundle/Controller/RemoveCatalogPromotionAction.php @@ -20,7 +20,6 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -/** @experimental */ final class RemoveCatalogPromotionAction { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/Controller/RemoveCustomerShopUserAction.php b/src/Sylius/Bundle/ApiBundle/Controller/RemoveCustomerShopUserAction.php new file mode 100644 index 0000000000..8d9288513d --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Controller/RemoveCustomerShopUserAction.php @@ -0,0 +1,49 @@ + $shopUserRepository + */ + public function __construct( + private MessageBusInterface $commandBus, + private UserRepositoryInterface $shopUserRepository, + ) { + } + + public function __invoke(Request $request): Response + { + $customerId = $request->attributes->get('id', ''); + + $user = $this->shopUserRepository->findOneBy(['customer' => $customerId]); + if (null === $user) { + throw new UserNotFoundException(); + } + + $this->commandBus->dispatch(new RemoveShopUser($user->getId())); + + return new JsonResponse(status: Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Controller/UploadAvatarImageAction.php b/src/Sylius/Bundle/ApiBundle/Controller/UploadAvatarImageAction.php index 8ead00a7a7..88c1b79126 100644 --- a/src/Sylius/Bundle/ApiBundle/Controller/UploadAvatarImageAction.php +++ b/src/Sylius/Bundle/ApiBundle/Controller/UploadAvatarImageAction.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Controller; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\ImageInterface; use Sylius\Component\Core\Repository\AvatarImageRepositoryInterface; @@ -24,7 +24,6 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Webmozart\Assert\Assert; -/** @experimental */ final class UploadAvatarImageAction { public function __construct( @@ -49,7 +48,7 @@ final class UploadAvatarImageAction Assert::notEmpty($ownerIri); /** @var ResourceInterface|AdminUserInterface $owner */ - $owner = $this->iriConverter->getItemFromIri($ownerIri); + $owner = $this->iriConverter->getResourceFromIri($ownerIri); Assert::isInstanceOf($owner, AdminUserInterface::class); $oldImage = $owner->getImage(); diff --git a/src/Sylius/Bundle/ApiBundle/Controller/UploadProductImageAction.php b/src/Sylius/Bundle/ApiBundle/Controller/UploadProductImageAction.php new file mode 100644 index 0000000000..7688fb496f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Controller/UploadProductImageAction.php @@ -0,0 +1,35 @@ +productImageCreator->create( + $request->attributes->get('code', ''), + $request->files->get('file'), + $request->request->get('type'), + ['productVariants' => $request->request->all('productVariants')], + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Controller/UploadTaxonImageAction.php b/src/Sylius/Bundle/ApiBundle/Controller/UploadTaxonImageAction.php new file mode 100644 index 0000000000..a2eafbb866 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Controller/UploadTaxonImageAction.php @@ -0,0 +1,34 @@ +taxonImageCreator->create( + $request->attributes->get('code', ''), + $request->files->get('file'), + $request->request->get('type'), + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Converter/IriToIdentifierConverter.php b/src/Sylius/Bundle/ApiBundle/Converter/IriToIdentifierConverter.php index 17cd40ddf3..a6de8eba14 100644 --- a/src/Sylius/Bundle/ApiBundle/Converter/IriToIdentifierConverter.php +++ b/src/Sylius/Bundle/ApiBundle/Converter/IriToIdentifierConverter.php @@ -24,8 +24,6 @@ use Symfony\Component\Routing\RouterInterface; /** * Logic of this class is based on ApiPlatform\Core\Bridge\Symfony\Routing\IriConverter, This class provide `id` from path but it doesn't fetch object from database - * - * @experimental */ final class IriToIdentifierConverter implements IriToIdentifierConverterInterface { diff --git a/src/Sylius/Bundle/ApiBundle/Converter/IriToIdentifierConverterInterface.php b/src/Sylius/Bundle/ApiBundle/Converter/IriToIdentifierConverterInterface.php index b5f96dec1c..1de4766c06 100644 --- a/src/Sylius/Bundle/ApiBundle/Converter/IriToIdentifierConverterInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Converter/IriToIdentifierConverterInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Converter; -/** @experimental */ interface IriToIdentifierConverterInterface { public function getIdentifier(?string $iri): ?string; diff --git a/src/Sylius/Bundle/ApiBundle/Creator/ImageCreatorInterface.php b/src/Sylius/Bundle/ApiBundle/Creator/ImageCreatorInterface.php new file mode 100644 index 0000000000..fd2d69c926 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Creator/ImageCreatorInterface.php @@ -0,0 +1,22 @@ + $context */ + public function create(string $ownerCode, ?\SplFileInfo $file, ?string $type, array $context = []): ImageInterface; +} diff --git a/src/Sylius/Bundle/ApiBundle/Creator/ProductImageCreator.php b/src/Sylius/Bundle/ApiBundle/Creator/ProductImageCreator.php new file mode 100644 index 0000000000..f89d828058 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Creator/ProductImageCreator.php @@ -0,0 +1,78 @@ + $productImageFactory + * @param ProductRepositoryInterface $productRepository + */ + public function __construct( + private FactoryInterface $productImageFactory, + private ProductRepositoryInterface $productRepository, + private ImageUploaderInterface $imageUploader, + private IriConverterInterface $iriConverter, + ) { + } + + /** @param array $context */ + public function create(string $ownerCode, ?\SplFileInfo $file, ?string $type, array $context = []): ImageInterface + { + if (null === $file) { + throw new NoFileUploadedException(); + } + + $owner = $this->productRepository->findOneBy(['code' => $ownerCode]); + if (null === $owner) { + throw new ProductNotFoundException(); + } + + $productImage = $this->productImageFactory->createNew(); + $productImage->setFile($file); + $productImage->setType($type); + + if ($context['productVariants']) { + $this->setProductVariants($productImage, $context['productVariants']); + } + + $owner->addImage($productImage); + + $this->imageUploader->upload($productImage); + + return $productImage; + } + + /** @param array $productVariantsIris */ + private function setProductVariants(ProductImageInterface $productImage, array $productVariantsIris): void + { + foreach ($productVariantsIris as $productVariantIri) { + $productVariant = $this->iriConverter->getResourceFromIri($productVariantIri); + Assert::isInstanceOf($productVariant, ProductVariantInterface::class); + $productImage->addProductVariant($productVariant); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Creator/TaxonImageCreator.php b/src/Sylius/Bundle/ApiBundle/Creator/TaxonImageCreator.php new file mode 100644 index 0000000000..d68d6d7b6e --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Creator/TaxonImageCreator.php @@ -0,0 +1,60 @@ + $taxonImageFactory + * @param TaxonRepositoryInterface $taxonRepository + */ + public function __construct( + private FactoryInterface $taxonImageFactory, + private TaxonRepositoryInterface $taxonRepository, + private ImageUploaderInterface $imageUploader, + ) { + } + + /** @param array $context */ + public function create(string $ownerCode, ?\SplFileInfo $file, ?string $type, array $context = []): ImageInterface + { + if (null === $file) { + throw new NoFileUploadedException(); + } + + $owner = $this->taxonRepository->findOneBy(['code' => $ownerCode]); + if (null === $owner) { + throw new TaxonNotFoundException(); + } + + $taxonImage = $this->taxonImageFactory->createNew(); + $taxonImage->setFile($file); + $taxonImage->setType($type); + + $owner->addImage($taxonImage); + + $this->imageUploader->upload($taxonImage); + + return $taxonImage; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/AddressDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/AddressDataPersister.php index 0b44894523..bffeb58761 100644 --- a/src/Sylius/Bundle/ApiBundle/DataPersister/AddressDataPersister.php +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/AddressDataPersister.php @@ -20,7 +20,6 @@ use Sylius\Component\Core\Model\AddressInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ShopUserInterface; -/** @experimental */ final class AddressDataPersister implements ContextAwareDataPersisterInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/AdminUserDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/AdminUserDataPersister.php index 701ac80ae6..4d3b0ff6e0 100644 --- a/src/Sylius/Bundle/ApiBundle/DataPersister/AdminUserDataPersister.php +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/AdminUserDataPersister.php @@ -20,7 +20,6 @@ use Sylius\Component\User\Model\UserInterface; use Sylius\Component\User\Security\PasswordUpdaterInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -/** @experimental */ final class AdminUserDataPersister implements ContextAwareDataPersisterInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/ChannelDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/ChannelDataPersister.php new file mode 100644 index 0000000000..ee070bf845 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/ChannelDataPersister.php @@ -0,0 +1,56 @@ + $context + */ + public function supports($data, array $context = []): bool + { + return $data instanceof ChannelInterface; + } + + /** + * @param array $context + */ + public function persist($data, array $context = []) + { + return $this->decoratedDataPersister->persist($data, $context); + } + + /** + * @param array $context + */ + public function remove($data, array $context = []): void + { + if (!$this->channelDeletionChecker->isDeletable($data)) { + throw new ChannelCannotBeRemoved('The channel cannot be deleted. At least one enabled channel is required.'); + } + + $this->decoratedDataPersister->remove($data, $context); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/CountryDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/CountryDataPersister.php index 7ca7ea2628..49d7ef75f5 100644 --- a/src/Sylius/Bundle/ApiBundle/DataPersister/CountryDataPersister.php +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/CountryDataPersister.php @@ -18,7 +18,6 @@ use Sylius\Bundle\ApiBundle\Exception\ProvinceCannotBeRemoved; use Sylius\Component\Addressing\Checker\CountryProvincesDeletionCheckerInterface; use Sylius\Component\Addressing\Model\CountryInterface; -/** @experimental */ final class CountryDataPersister implements ContextAwareDataPersisterInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/CustomerDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/CustomerDataPersister.php new file mode 100644 index 0000000000..181ee9bc03 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/CustomerDataPersister.php @@ -0,0 +1,53 @@ + $context */ + public function supports($data, array $context = []): bool + { + return $data instanceof CustomerInterface; + } + + /** + * @param CustomerInterface $data + * @param array $context + */ + public function persist($data, array $context = []): void + { + $user = $data->getUser(); + if (null !== $user && null !== $user->getPlainPassword()) { + $this->passwordUpdater->updatePassword($user); + } + + $this->decoratedDataPersister->persist($data, $context); + } + + /** @param array $context */ + public function remove($data, array $context = []): void + { + $this->decoratedDataPersister->remove($data, $context); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/LocaleDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/LocaleDataPersister.php new file mode 100644 index 0000000000..bd41ba8601 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/LocaleDataPersister.php @@ -0,0 +1,59 @@ + $context + */ + public function supports($data, array $context = []): bool + { + return $data instanceof LocaleInterface; + } + + /** + * @param array $context + */ + public function persist($data, array $context = []): object + { + return $this->decoratedDataPersister->persist($data, $context); + } + + /** + * @param LocaleInterface $data + * @param array $context + * + * @throws LocaleIsUsedException + */ + public function remove($data, array $context = []): mixed + { + if ($this->localeUsageChecker->isUsed($data->getCode())) { + throw new LocaleIsUsedException($data->getCode()); + } + + return $this->decoratedDataPersister->remove($data, $context); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/MessengerDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/MessengerDataPersister.php index 58ee1d2377..b470c15af0 100644 --- a/src/Sylius/Bundle/ApiBundle/DataPersister/MessengerDataPersister.php +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/MessengerDataPersister.php @@ -17,7 +17,6 @@ use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface; use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException; use Symfony\Component\Messenger\Exception\HandlerFailedException; -/** @experimental */ final class MessengerDataPersister implements ContextAwareDataPersisterInterface { public function __construct(private ContextAwareDataPersisterInterface $decoratedDataPersister) diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/PaymentMethodDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/PaymentMethodDataPersister.php new file mode 100644 index 0000000000..a09e2517d2 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/PaymentMethodDataPersister.php @@ -0,0 +1,48 @@ + $context */ + public function supports($data, array $context = []): bool + { + return $data instanceof PaymentMethodInterface; + } + + /** @param array $context */ + public function persist($data, array $context = []) + { + return $this->decoratedDataPersister->persist($data, $context); + } + + /** @param array $context */ + public function remove($data, array $context = []): void + { + try { + $this->decoratedDataPersister->remove($data, $context); + } catch (ForeignKeyConstraintViolationException) { + throw new PaymentMethodCannotBeRemoved(); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php new file mode 100644 index 0000000000..8926b115b3 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/ProductAttributeDataPersister.php @@ -0,0 +1,47 @@ +decoratedDataPersister->persist($data, $context); + } + + public function remove($data, array $context = []) + { + try { + return $this->decoratedDataPersister->remove($data, $context); + } catch (ForeignKeyConstraintViolationException) { + throw new ProductAttributeCannotBeRemoved(); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/ProductDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/ProductDataPersister.php new file mode 100644 index 0000000000..a08549140d --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/ProductDataPersister.php @@ -0,0 +1,49 @@ + $context */ + public function supports($data, array $context = []): bool + { + return $data instanceof ProductInterface; + } + + /** @param array $context */ + public function persist($data, array $context = []) + { + return $this->decoratedDataPersister->persist($data, $context); + } + + /** @param array $context */ + public function remove($data, array $context = []) + { + try { + return $this->decoratedDataPersister->remove($data, $context); + } catch (ForeignKeyConstraintViolationException) { + throw new ProductCannotBeRemoved(); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/ProductTaxonDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/ProductTaxonDataPersister.php new file mode 100644 index 0000000000..eee3590446 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/ProductTaxonDataPersister.php @@ -0,0 +1,53 @@ +getProduct(); + $product->addProductTaxon($data); + + $this->decoratedDataPersister->persist($data, $context); + + $this->eventBus->dispatch(new ProductUpdated($product->getCode())); + } + + /** @param ProductTaxonInterface $data */ + public function remove($data, array $context = []) + { + $this->decoratedDataPersister->remove($data, $context); + + $this->eventBus->dispatch(new ProductUpdated($data->getProduct()->getCode())); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/ProductVariantDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/ProductVariantDataPersister.php new file mode 100644 index 0000000000..7b7dc150ab --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/ProductVariantDataPersister.php @@ -0,0 +1,48 @@ + $context */ + public function supports($data, array $context = []): bool + { + return $data instanceof ProductVariantInterface; + } + + /** @param array $context */ + public function persist($data, array $context = []) + { + return $this->decoratedDataPersister->persist($data, $context); + } + + /** @param array $context */ + public function remove($data, array $context = []) + { + try { + return $this->decoratedDataPersister->remove($data, $context); + } catch (ForeignKeyConstraintViolationException) { + throw new ProductVariantCannotBeRemoved(); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/PromotionCouponDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/PromotionCouponDataPersister.php new file mode 100644 index 0000000000..d5a9a96a3f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/PromotionCouponDataPersister.php @@ -0,0 +1,49 @@ + $context */ + public function supports($data, array $context = []): bool + { + return $data instanceof PromotionCouponInterface; + } + + /** @param array $context */ + public function persist($data, array $context = []) + { + return $this->decoratedDataPersister->persist($data, $context); + } + + /** @param array $context */ + public function remove($data, array $context = []): void + { + try { + $this->decoratedDataPersister->remove($data, $context); + } catch (ForeignKeyConstraintViolationException) { + throw new PromotionCouponCannotBeRemoved(); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/PromotionDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/PromotionDataPersister.php new file mode 100644 index 0000000000..075922675c --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/PromotionDataPersister.php @@ -0,0 +1,48 @@ + $context */ + public function supports($data, array $context = []): bool + { + return $data instanceof PromotionInterface; + } + + /** @param array $context */ + public function persist($data, array $context = []) + { + return $this->decoratedDataPersister->persist($data, $context); + } + + /** @param array $context */ + public function remove($data, array $context = []): void + { + try { + $this->decoratedDataPersister->remove($data, $context); + } catch (ForeignKeyConstraintViolationException) { + throw new PromotionCannotBeRemoved(); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/ShippingMethodDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/ShippingMethodDataPersister.php index 9b1c93fa8e..c265c7afb9 100644 --- a/src/Sylius/Bundle/ApiBundle/DataPersister/ShippingMethodDataPersister.php +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/ShippingMethodDataPersister.php @@ -18,7 +18,6 @@ use Doctrine\DBAL\Exception\ForeignKeyConstraintViolationException; use Sylius\Bundle\ApiBundle\Exception\ShippingMethodCannotBeRemoved; use Sylius\Component\Core\Model\ShippingMethodInterface; -/** @experimental */ final class ShippingMethodDataPersister implements ContextAwareDataPersisterInterface { public function __construct(private ContextAwareDataPersisterInterface $decoratedDataPersister) diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/TranslatableDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/TranslatableDataPersister.php new file mode 100644 index 0000000000..6111461ac3 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/TranslatableDataPersister.php @@ -0,0 +1,67 @@ + $context + */ + public function supports($data, array $context = []): bool + { + return $data instanceof TranslatableInterface; + } + + /** + * @param TranslatableInterface $data + * @param array $context + */ + public function persist($data, array $context = []): object + { + $defaultLocaleCode = $this->localeProvider->getDefaultLocaleCode(); + + if (!$data->getTranslations()->containsKey($defaultLocaleCode)) { + throw new TranslationInDefaultLocaleCannotBeRemoved( + sprintf('Translation in the default locale "%s" cannot be removed.', $defaultLocaleCode), + ); + } + + return $data; + } + + /** + * @param TranslatableInterface $data + * @param array $context + */ + public function remove($data, array $context = []): mixed + { + return $data; + } + + /** @param array $context */ + public function resumable(array $context = []): bool + { + return true; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataPersister/ZoneDataPersister.php b/src/Sylius/Bundle/ApiBundle/DataPersister/ZoneDataPersister.php index 15a747670b..a20e064e11 100644 --- a/src/Sylius/Bundle/ApiBundle/DataPersister/ZoneDataPersister.php +++ b/src/Sylius/Bundle/ApiBundle/DataPersister/ZoneDataPersister.php @@ -18,7 +18,6 @@ use Sylius\Bundle\ApiBundle\Exception\ZoneCannotBeRemoved; use Sylius\Component\Addressing\Checker\ZoneDeletionCheckerInterface; use Sylius\Component\Addressing\Model\ZoneInterface; -/** @experimental */ final class ZoneDataPersister implements ContextAwareDataPersisterInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/AccountResetPasswordItemDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/AccountResetPasswordItemDataProvider.php index 73ff23f491..de79361f18 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/AccountResetPasswordItemDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/AccountResetPasswordItemDataProvider.php @@ -17,7 +17,6 @@ use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use Sylius\Bundle\ApiBundle\Command\Account\ResetPassword; -/** @experimental */ final class AccountResetPasswordItemDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface { public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/AdminOrderItemAdjustmentsSubresourceDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/AdminOrderItemAdjustmentsSubresourceDataProvider.php new file mode 100644 index 0000000000..f426b313ab --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/AdminOrderItemAdjustmentsSubresourceDataProvider.php @@ -0,0 +1,58 @@ + $orderItemRepository */ + public function __construct( + private readonly OrderItemRepositoryInterface $orderItemRepository, + private readonly SectionProviderInterface $sectionProvider, + ) { + } + + /** @param array $context */ + public function supports(string $resourceClass, string $operationName = null, array $context = []): bool + { + return + is_a($resourceClass, AdjustmentInterface::class, true) && + $this->sectionProvider->getSection() instanceof AdminApiSection && + is_a(array_key_first($context['subresource_resources']), OrderItemInterface::class, true) + ; + } + + /** + * @param array $identifiers + * @param array $context + * + * @return iterable + */ + public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null): iterable + { + $orderItem = $this->orderItemRepository->find(reset($context['subresource_identifiers'])); + if (null === $orderItem) { + return []; + } + + return $orderItem->getAdjustmentsRecursively(); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/AdminResetPasswordItemDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/AdminResetPasswordItemDataProvider.php index 0dd86399f2..1b2fda9fce 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/AdminResetPasswordItemDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/AdminResetPasswordItemDataProvider.php @@ -17,7 +17,6 @@ use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use Sylius\Bundle\CoreBundle\Message\Admin\Account\ResetPassword; -/** @experimental */ final class AdminResetPasswordItemDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface { public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/ChannelAwareItemDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/ChannelAwareItemDataProvider.php index 7aae4380dd..09bc8a7da5 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/ChannelAwareItemDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/ChannelAwareItemDataProvider.php @@ -18,7 +18,6 @@ use Sylius\Bundle\ApiBundle\Serializer\ContextKeys; use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Channel\Context\ChannelNotFoundException; -/** @experimental */ final class ChannelAwareItemDataProvider implements ItemDataProviderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/ChannelsCollectionDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/ChannelsCollectionDataProvider.php index ac7870ba37..2bffb0ce41 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/ChannelsCollectionDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/ChannelsCollectionDataProvider.php @@ -18,7 +18,6 @@ use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Core\Model\ChannelInterface; -/** @experimental */ final class ChannelsCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface { public function __construct(private ChannelContextInterface $channelContext) diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/CustomerItemDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/CustomerItemDataProvider.php index 085026c32c..56269567b7 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/CustomerItemDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/CustomerItemDataProvider.php @@ -22,7 +22,6 @@ use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Core\Repository\CustomerRepositoryInterface; use Symfony\Component\Security\Core\User\UserInterface as SymfonyUserInterface; -/** @experimental */ final class CustomerItemDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/OrderAdjustmentsSubresourceDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/OrderAdjustmentsSubresourceDataProvider.php index dcdfed4df6..20816895b4 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/OrderAdjustmentsSubresourceDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/OrderAdjustmentsSubresourceDataProvider.php @@ -20,7 +20,6 @@ use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Sylius\Component\Order\Model\AdjustmentInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class OrderAdjustmentsSubresourceDataProvider implements RestrictedDataProviderInterface, SubresourceDataProviderInterface { public function __construct(private OrderRepositoryInterface $orderRepository) diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemAdjustmentsSubresourceDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemAdjustmentsSubresourceDataProvider.php index bb5ab40914..eb04d6b9bc 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemAdjustmentsSubresourceDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemAdjustmentsSubresourceDataProvider.php @@ -16,17 +16,17 @@ namespace Sylius\Bundle\ApiBundle\DataProvider; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; use Sylius\Component\Core\Model\OrderItemInterface; +use Sylius\Component\Core\Repository\OrderItemRepositoryInterface; use Sylius\Component\Order\Model\AdjustmentInterface; -use Sylius\Component\Order\Repository\OrderItemRepositoryInterface; -use Webmozart\Assert\Assert; -/** @experimental */ final class OrderItemAdjustmentsSubresourceDataProvider implements RestrictedDataProviderInterface, SubresourceDataProviderInterface { - public function __construct(private OrderItemRepositoryInterface $orderItemRepository) + /** @param OrderItemRepositoryInterface $orderItemRepository */ + public function __construct(private readonly OrderItemRepositoryInterface $orderItemRepository) { } + /** @param array $context */ public function supports(string $resourceClass, string $operationName = null, array $context = []): bool { $subresourceIdentifiers = $context['subresource_identifiers'] ?? null; @@ -37,13 +37,23 @@ final class OrderItemAdjustmentsSubresourceDataProvider implements RestrictedDat ; } - public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null) + /** + * @param array $identifiers + * @param array $context + * + * @return iterable + */ + public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null): iterable { $subresourceIdentifiers = $context['subresource_identifiers']; - /** @var OrderItemInterface|null $orderItem */ - $orderItem = $this->orderItemRepository->find($subresourceIdentifiers['items']); - Assert::notNull($orderItem); + $orderItem = $this->orderItemRepository->findOneByIdAndOrderTokenValue( + (int) $subresourceIdentifiers['items'], + $subresourceIdentifiers['tokenValue'], + ); + if (null === $orderItem) { + return []; + } return $orderItem->getAdjustmentsRecursively(); } diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemItemDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemItemDataProvider.php index 02236679e8..ec872cb46d 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemItemDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemItemDataProvider.php @@ -22,7 +22,6 @@ use Sylius\Component\Core\Model\OrderItemInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Core\Repository\OrderItemRepositoryInterface; -/** @experimental */ final class OrderItemItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemUnitItemDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemUnitItemDataProvider.php index 793fb957cc..cf0e0614fb 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemUnitItemDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/OrderItemUnitItemDataProvider.php @@ -22,7 +22,6 @@ use Sylius\Component\Core\Model\OrderItemUnitInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Core\Repository\OrderItemUnitRepositoryInterface; -/** @experimental */ final class OrderItemUnitItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/PaymentItemDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/PaymentItemDataProvider.php index 701478d971..d45741c023 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/PaymentItemDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/PaymentItemDataProvider.php @@ -22,7 +22,6 @@ use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Core\Repository\PaymentRepositoryInterface; -/** @experimental */ final class PaymentItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/PaymentMethodsCollectionDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/PaymentMethodsCollectionDataProvider.php index 4984e10be7..013d1ba059 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/PaymentMethodsCollectionDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/PaymentMethodsCollectionDataProvider.php @@ -25,7 +25,6 @@ use Sylius\Component\Core\Repository\PaymentMethodRepositoryInterface; use Sylius\Component\Core\Repository\PaymentRepositoryInterface; use Sylius\Component\Payment\Resolver\PaymentMethodsResolverInterface; -/** @experimental */ final class PaymentMethodsCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/ProductAttributesSubresourceDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/ProductAttributesSubresourceDataProvider.php index f426d55bbf..1e26935f9e 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/ProductAttributesSubresourceDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/ProductAttributesSubresourceDataProvider.php @@ -22,7 +22,6 @@ use Sylius\Component\Locale\Provider\LocaleProviderInterface; use Sylius\Component\Product\Model\ProductAttributeValueInterface; use Sylius\Component\Product\Repository\ProductAttributeValueRepositoryInterface; -/** @experimental */ final class ProductAttributesSubresourceDataProvider implements RestrictedDataProviderInterface, SubresourceDataProviderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/ProductItemDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/ProductItemDataProvider.php index 5cc8dd8bbb..d95a42f466 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/ProductItemDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/ProductItemDataProvider.php @@ -23,7 +23,6 @@ use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Repository\ProductRepositoryInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class ProductItemDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface { public function __construct( @@ -45,7 +44,7 @@ final class ProductItemDataProvider implements RestrictedDataProviderInterface, /** @var ChannelInterface $channel */ $channel = $context[ContextKeys::CHANNEL]; - return $this->productRepository->findOneByChannelAndCode($channel, $id); + return $this->productRepository->findOneByChannelAndCodeWithAvailableAssociations($channel, $id); } public function supports(string $resourceClass, string $operationName = null, array $context = []): bool diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/ShipmentItemDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/ShipmentItemDataProvider.php index f402b3f944..6d7521e6ea 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/ShipmentItemDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/ShipmentItemDataProvider.php @@ -22,7 +22,6 @@ use Sylius\Component\Core\Model\ShipmentInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Core\Repository\ShipmentRepositoryInterface; -/** @experimental */ final class ShipmentItemDataProvider implements ItemDataProviderInterface, RestrictedDataProviderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/ShippingMethodsCollectionDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/ShippingMethodsCollectionDataProvider.php index dca81657ed..40a8c3d519 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/ShippingMethodsCollectionDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/ShippingMethodsCollectionDataProvider.php @@ -23,7 +23,6 @@ use Sylius\Component\Core\Repository\ShipmentRepositoryInterface; use Sylius\Component\Core\Repository\ShippingMethodRepositoryInterface; use Sylius\Component\Shipping\Resolver\ShippingMethodsResolverInterface; -/** @experimental */ final class ShippingMethodsCollectionDataProvider implements ContextAwareCollectionDataProviderInterface, RestrictedDataProviderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/DataProvider/VerifyCustomerAccountItemDataProvider.php b/src/Sylius/Bundle/ApiBundle/DataProvider/VerifyCustomerAccountItemDataProvider.php index 7ebae684d6..e2391a41dd 100644 --- a/src/Sylius/Bundle/ApiBundle/DataProvider/VerifyCustomerAccountItemDataProvider.php +++ b/src/Sylius/Bundle/ApiBundle/DataProvider/VerifyCustomerAccountItemDataProvider.php @@ -17,7 +17,6 @@ use ApiPlatform\Core\DataProvider\ItemDataProviderInterface; use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; use Sylius\Bundle\ApiBundle\Command\Account\VerifyCustomerAccount; -/** @experimental */ final class VerifyCustomerAccountItemDataProvider implements RestrictedDataProviderInterface, ItemDataProviderInterface { public function getItem(string $resourceClass, $id, string $operationName = null, array $context = []) diff --git a/src/Sylius/Bundle/ApiBundle/DataTransformer/ChannelCodeAwareInputCommandDataTransformer.php b/src/Sylius/Bundle/ApiBundle/DataTransformer/ChannelCodeAwareInputCommandDataTransformer.php index 768f336792..8aa240236a 100644 --- a/src/Sylius/Bundle/ApiBundle/DataTransformer/ChannelCodeAwareInputCommandDataTransformer.php +++ b/src/Sylius/Bundle/ApiBundle/DataTransformer/ChannelCodeAwareInputCommandDataTransformer.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\DataTransformer; use Sylius\Bundle\ApiBundle\Command\ChannelCodeAwareInterface; use Sylius\Component\Channel\Context\ChannelContextInterface; -/** @experimental */ final class ChannelCodeAwareInputCommandDataTransformer implements CommandDataTransformerInterface { public function __construct(private ChannelContextInterface $channelContext) diff --git a/src/Sylius/Bundle/ApiBundle/DataTransformer/CommandAwareInputDataTransformer.php b/src/Sylius/Bundle/ApiBundle/DataTransformer/CommandAwareInputDataTransformer.php index 3cef21c19f..07f31c3744 100644 --- a/src/Sylius/Bundle/ApiBundle/DataTransformer/CommandAwareInputDataTransformer.php +++ b/src/Sylius/Bundle/ApiBundle/DataTransformer/CommandAwareInputDataTransformer.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\DataTransformer; use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use Sylius\Bundle\ApiBundle\Command\CommandAwareDataTransformerInterface; -/** @experimental */ final class CommandAwareInputDataTransformer implements DataTransformerInterface { /** @var CommandDataTransformerInterface[] */ diff --git a/src/Sylius/Bundle/ApiBundle/DataTransformer/CommandDataTransformerInterface.php b/src/Sylius/Bundle/ApiBundle/DataTransformer/CommandDataTransformerInterface.php index b3a48d87ab..fd082e3590 100644 --- a/src/Sylius/Bundle/ApiBundle/DataTransformer/CommandDataTransformerInterface.php +++ b/src/Sylius/Bundle/ApiBundle/DataTransformer/CommandDataTransformerInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\DataTransformer; -/** @experimental */ interface CommandDataTransformerInterface { public function transform($object, string $to, array $context = []); diff --git a/src/Sylius/Bundle/ApiBundle/DataTransformer/LocaleCodeAwareInputCommandDataTransformer.php b/src/Sylius/Bundle/ApiBundle/DataTransformer/LocaleCodeAwareInputCommandDataTransformer.php index 3fbedd730e..64c42328f1 100644 --- a/src/Sylius/Bundle/ApiBundle/DataTransformer/LocaleCodeAwareInputCommandDataTransformer.php +++ b/src/Sylius/Bundle/ApiBundle/DataTransformer/LocaleCodeAwareInputCommandDataTransformer.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\DataTransformer; use Sylius\Bundle\ApiBundle\Command\LocaleCodeAwareInterface; use Sylius\Component\Locale\Context\LocaleContextInterface; -/** @experimental */ final class LocaleCodeAwareInputCommandDataTransformer implements CommandDataTransformerInterface { public function __construct(private LocaleContextInterface $localeContext) diff --git a/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInCustomerEmailAwareCommandDataTransformer.php b/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInCustomerEmailAwareCommandDataTransformer.php index cffc3a7043..59ef110f25 100644 --- a/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInCustomerEmailAwareCommandDataTransformer.php +++ b/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInCustomerEmailAwareCommandDataTransformer.php @@ -20,7 +20,6 @@ use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\User\Model\UserInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class LoggedInCustomerEmailAwareCommandDataTransformer implements CommandDataTransformerInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInCustomerEmailIfNotSetAwareCommandDataTransformer.php b/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInCustomerEmailIfNotSetAwareCommandDataTransformer.php index 417b8c67b0..f324496c87 100644 --- a/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInCustomerEmailIfNotSetAwareCommandDataTransformer.php +++ b/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInCustomerEmailIfNotSetAwareCommandDataTransformer.php @@ -21,7 +21,6 @@ use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\User\Model\UserInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class LoggedInCustomerEmailIfNotSetAwareCommandDataTransformer implements CommandDataTransformerInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInShopUserIdAwareCommandDataTransformer.php b/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInShopUserIdAwareCommandDataTransformer.php index 5f7344624f..b44944e316 100644 --- a/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInShopUserIdAwareCommandDataTransformer.php +++ b/src/Sylius/Bundle/ApiBundle/DataTransformer/LoggedInShopUserIdAwareCommandDataTransformer.php @@ -18,7 +18,6 @@ use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\User\Model\UserInterface; -/** @experimental */ final class LoggedInShopUserIdAwareCommandDataTransformer implements CommandDataTransformerInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/DataTransformer/OrderTokenValueAwareInputCommandDataTransformer.php b/src/Sylius/Bundle/ApiBundle/DataTransformer/OrderTokenValueAwareInputCommandDataTransformer.php index e6b419afd5..2de980838d 100644 --- a/src/Sylius/Bundle/ApiBundle/DataTransformer/OrderTokenValueAwareInputCommandDataTransformer.php +++ b/src/Sylius/Bundle/ApiBundle/DataTransformer/OrderTokenValueAwareInputCommandDataTransformer.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\DataTransformer; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; use Sylius\Component\Core\Model\OrderInterface; -/** @experimental */ final class OrderTokenValueAwareInputCommandDataTransformer implements CommandDataTransformerInterface { public function transform($object, string $to, array $context = []) diff --git a/src/Sylius/Bundle/ApiBundle/DataTransformer/ShipmentIdAwareInputCommandDataTransformer.php b/src/Sylius/Bundle/ApiBundle/DataTransformer/ShipmentIdAwareInputCommandDataTransformer.php index 23b96df296..e0a9fbd696 100644 --- a/src/Sylius/Bundle/ApiBundle/DataTransformer/ShipmentIdAwareInputCommandDataTransformer.php +++ b/src/Sylius/Bundle/ApiBundle/DataTransformer/ShipmentIdAwareInputCommandDataTransformer.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\DataTransformer; use Sylius\Bundle\ApiBundle\Command\ShipmentIdAwareInterface; use Sylius\Component\Core\Model\ShipmentInterface; -/** @experimental */ final class ShipmentIdAwareInputCommandDataTransformer implements CommandDataTransformerInterface { public function transform($object, string $to, array $context = []) diff --git a/src/Sylius/Bundle/ApiBundle/DataTransformer/SubresourceIdAwareCommandDataTransformer.php b/src/Sylius/Bundle/ApiBundle/DataTransformer/SubresourceIdAwareCommandDataTransformer.php index 89f17ab87a..2464bd7380 100644 --- a/src/Sylius/Bundle/ApiBundle/DataTransformer/SubresourceIdAwareCommandDataTransformer.php +++ b/src/Sylius/Bundle/ApiBundle/DataTransformer/SubresourceIdAwareCommandDataTransformer.php @@ -17,7 +17,6 @@ use Sylius\Bundle\ApiBundle\Command\SubresourceIdAwareInterface; use Symfony\Component\HttpFoundation\RequestStack; use Webmozart\Assert\Assert; -/** @experimental */ final class SubresourceIdAwareCommandDataTransformer implements CommandDataTransformerInterface { public function __construct(private RequestStack $requestStack) diff --git a/src/Sylius/Bundle/ApiBundle/DependencyInjection/Compiler/CommandDataTransformerPass.php b/src/Sylius/Bundle/ApiBundle/DependencyInjection/Compiler/CommandDataTransformerPass.php index 74db58bab9..8abca9c3ee 100644 --- a/src/Sylius/Bundle/ApiBundle/DependencyInjection/Compiler/CommandDataTransformerPass.php +++ b/src/Sylius/Bundle/ApiBundle/DependencyInjection/Compiler/CommandDataTransformerPass.php @@ -19,7 +19,6 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -/** @experimental */ final class CommandDataTransformerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) diff --git a/src/Sylius/Bundle/ApiBundle/DependencyInjection/Compiler/ExtractorMergingCompilerPass.php b/src/Sylius/Bundle/ApiBundle/DependencyInjection/Compiler/ExtractorMergingCompilerPass.php new file mode 100644 index 0000000000..a81eb7ad05 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DependencyInjection/Compiler/ExtractorMergingCompilerPass.php @@ -0,0 +1,32 @@ +hasDefinition('api_platform.metadata.extractor.xml.legacy')) { + $definition = $container->getDefinition('api_platform.metadata.extractor.xml.legacy'); + $definition->setClass(MergingXmlExtractor::class); + $definition->addArgument(new Reference(LegacyResourceMetadataMerger::class)); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPass.php b/src/Sylius/Bundle/ApiBundle/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPass.php new file mode 100644 index 0000000000..753c262c66 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPass.php @@ -0,0 +1,73 @@ + 'Sylius\Bundle\ApiBundle\Validator\ResourceApiInputDataPropertiesValidatorInterface', + 'Sylius\PriceHistoryPlugin\Infrastructure\Serializer\ChannelDenormalizer' => 'Sylius\Bundle\ApiBundle\Serializer\ChannelDenormalizer', + 'Sylius\PriceHistoryPlugin\Infrastructure\Serializer\ChannelPriceHistoryConfigDenormalizer' => 'Sylius\Bundle\ApiBundle\Serializer\ChannelPriceHistoryConfigDenormalizer', + 'Sylius\PriceHistoryPlugin\Infrastructure\Serializer\ProductVariantNormalizer' => 'Sylius\Bundle\ApiBundle\Serializer\ProductVariantNormalizer', + ]; + + public function process(ContainerBuilder $container): void + { + foreach ($container->getDefinitions() as $serviceName => $definition) { + if ($this->shouldHaveLegacyAlias($container, $serviceName)) { + $this->addLegacyAlias($container, $serviceName); + } + } + } + + private function shouldHaveLegacyAlias(ContainerBuilder $container, string $serviceName): bool + { + foreach (self::PLUGIN_TO_SYLIUS_PREFIXES_MAP as $legacyPrefix => $syliusPrefix) { + $legacyServiceName = $this->getLegacyServiceName($serviceName); + + if (str_starts_with($serviceName, $syliusPrefix) && !$container->has($legacyServiceName)) { + return true; + } + } + + return false; + } + + private function addLegacyAlias(ContainerBuilder $container, string $serviceName): void + { + $legacyServiceName = $this->getLegacyServiceName($serviceName); + + $container + ->setAlias($legacyServiceName, $serviceName) + ->setPublic(true) + ->setDeprecated( + 'sylius/sylius', + '1.13', + sprintf('Alias %%alias_id%% is deprecated. Consider using the %s service.', $serviceName), + ) + ; + } + + private function getLegacyServiceName(string $name): string + { + return str_replace( + array_values(self::PLUGIN_TO_SYLIUS_PREFIXES_MAP), + array_keys(self::PLUGIN_TO_SYLIUS_PREFIXES_MAP), + $name, + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/ApiBundle/DependencyInjection/Configuration.php index 2fe8e17ceb..254b691eba 100644 --- a/src/Sylius/Bundle/ApiBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/ApiBundle/DependencyInjection/Configuration.php @@ -17,7 +17,6 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -/** @experimental */ final class Configuration implements ConfigurationInterface { public function getConfigTreeBuilder(): TreeBuilder @@ -36,6 +35,24 @@ final class Configuration implements ConfigurationInterface ->defaultFalse() ->end() ->end() + ->children() + ->arrayNode('order_states_to_filter_out') + ->scalarPrototype()->end() + ->end() + ->end() + ->children() + ->arrayNode('serialization_groups') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('skip_adding_read_group') + ->defaultFalse() + ->end() + ->booleanNode('skip_adding_index_and_show_groups') + ->defaultFalse() + ->end() + ->end() + ->end() + ->end() ->children() ->variableNode('product_image_prefix') ->defaultValue('media/image') diff --git a/src/Sylius/Bundle/ApiBundle/DependencyInjection/SyliusApiExtension.php b/src/Sylius/Bundle/ApiBundle/DependencyInjection/SyliusApiExtension.php index e435b85426..03ed173d0c 100644 --- a/src/Sylius/Bundle/ApiBundle/DependencyInjection/SyliusApiExtension.php +++ b/src/Sylius/Bundle/ApiBundle/DependencyInjection/SyliusApiExtension.php @@ -13,13 +13,16 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\DependencyInjection; +use Sylius\Bundle\ApiBundle\Attribute\AsCommandDataTransformer; +use Sylius\Bundle\ApiBundle\Attribute\AsDocumentationModifier; +use Sylius\Bundle\ApiBundle\Attribute\AsPaymentConfigurationProvider; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; -/** @experimental */ final class SyliusApiExtension extends Extension implements PrependExtensionInterface { public function load(array $configs, ContainerBuilder $container): void @@ -34,12 +37,20 @@ final class SyliusApiExtension extends Extension implements PrependExtensionInte 'sylius_api.filter_eager_loading_extension.restricted_resources', $config['filter_eager_loading_extension']['restricted_resources'], ); + $container->setParameter('sylius_api.order_states_to_filter_out', $config['order_states_to_filter_out']); + $container->setParameter('sylius_api.serialization_groups.skip_adding_read_group', $config['serialization_groups']['skip_adding_read_group']); + $container->setParameter('sylius_api.serialization_groups.skip_adding_index_and_show_groups', $config['serialization_groups']['skip_adding_index_and_show_groups']); $loader->load('services.xml'); - if ($container->hasParameter('api_platform.enable_swagger_ui') && $container->getParameter('api_platform.enable_swagger_ui')) { + // If parameter is not set, it means that Swagger is not enabled (api_platform.enable_swagger set to false) + $swaggerEnabled = $container->hasParameter('api_platform.swagger.api_keys'); + + if ($swaggerEnabled) { $loader->load('integrations/swagger.xml'); } + + $this->registerAutoconfiguration($container); } public function prepend(ContainerBuilder $container): void @@ -56,4 +67,28 @@ final class SyliusApiExtension extends Extension implements PrependExtensionInte $container->prependExtensionConfig('api_platform', ['mapping' => ['paths' => [$path]]]); } + + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsCommandDataTransformer::class, + static function (ChildDefinition $definition, AsCommandDataTransformer $attribute): void { + $definition->addTag(AsCommandDataTransformer::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsDocumentationModifier::class, + static function (ChildDefinition $definition, AsDocumentationModifier $attribute): void { + $definition->addTag(AsDocumentationModifier::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsPaymentConfigurationProvider::class, + static function (ChildDefinition $definition, AsPaymentConfigurationProvider $attribute): void { + $definition->addTag(AsPaymentConfigurationProvider::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + } } diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AcceptedProductReviewsExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AcceptedProductReviewsExtension.php index 20541606ed..47374883fc 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AcceptedProductReviewsExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AcceptedProductReviewsExtension.php @@ -18,7 +18,6 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; use Sylius\Component\Review\Model\ReviewInterface; -/** @experimental */ final class AcceptedProductReviewsExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private string $productReviewClass) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AddressesExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AddressesExtension.php index 90c6f209dc..149fad6daa 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AddressesExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AddressesExtension.php @@ -23,7 +23,6 @@ use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -/** @experimental */ final class AddressesExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AvailableProductAssociationsInProductCollectionExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AvailableProductAssociationsInProductCollectionExtension.php new file mode 100644 index 0000000000..62316611f2 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/AvailableProductAssociationsInProductCollectionExtension.php @@ -0,0 +1,67 @@ +userContext->getUser(); + if ($user !== null && in_array('ROLE_API_ACCESS', $user->getRoles(), true)) { + return; + } + + $rootAlias = $queryBuilder->getRootAliases()[0]; + $enabledParameterName = $queryNameGenerator->generateParameterName('enabled'); + $associationAliasName = $queryNameGenerator->generateJoinAlias('association'); + $productAssociationAliasName = $queryNameGenerator->generateJoinAlias('associatedProduct'); + + $queryBuilder + ->addSelect($rootAlias) + ->addSelect($associationAliasName) + ->leftJoin(sprintf('%s.associations', $rootAlias), $associationAliasName) + ->leftJoin( + sprintf('%s.associatedProducts', $associationAliasName), + $productAssociationAliasName, + 'WITH', + $queryBuilder->expr()->andX( + $queryBuilder->expr()->eq(sprintf('%s.enabled', $productAssociationAliasName), 'true'), + $queryBuilder->expr()->eq(sprintf('%s.owner', $associationAliasName), $rootAlias), + ), + ) + ->andWhere(sprintf('%s.associations IS EMPTY OR %s.id IS NOT NULL', $rootAlias, $productAssociationAliasName)) + ->andWhere(sprintf('%s.enabled = :%s', $rootAlias, $enabledParameterName)) + ->setParameter($enabledParameterName, true) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/CountryCollectionExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/CountryCollectionExtension.php index e588e7feb5..f4839f7f9d 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/CountryCollectionExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/CountryCollectionExtension.php @@ -22,7 +22,6 @@ use Sylius\Component\Addressing\Model\CountryInterface; use Sylius\Component\Core\Model\AdminUserInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class CountryCollectionExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/CurrencyCollectionExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/CurrencyCollectionExtension.php index 38bd32e110..a98b426d8a 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/CurrencyCollectionExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/CurrencyCollectionExtension.php @@ -22,7 +22,6 @@ use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Currency\Model\CurrencyInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class CurrencyCollectionExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/EnabledProductVariantsExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/EnabledProductVariantsExtension.php index ce90aad45f..2ab0c5d696 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/EnabledProductVariantsExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/EnabledProductVariantsExtension.php @@ -19,7 +19,6 @@ use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Component\Core\Model\ProductVariantInterface; -/** @experimental */ final class EnabledProductVariantsExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtension.php new file mode 100644 index 0000000000..e555c1c6dc --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtension.php @@ -0,0 +1,46 @@ + $context */ + public function applyToCollection( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + string $operationName = null, + array $context = [], + ): void { + if ($this->promotionClass !== $resourceClass) { + return; + } + + if (isset($context['filters']['exists']['archivedAt'])) { + return; + } + + $rootAlias = $queryBuilder->getRootAliases()[0]; + + $queryBuilder->andWhere($queryBuilder->expr()->isNull(sprintf('%s.archivedAt', $rootAlias))); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedShippingMethodExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedShippingMethodExtension.php index aa8d6032a6..9fdcc5222b 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedShippingMethodExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/HideArchivedShippingMethodExtension.php @@ -17,7 +17,6 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\ContextAwareQueryCollectionEx use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; -/** @experimental */ final class HideArchivedShippingMethodExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private string $shippingMethodClass) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/LocaleCollectionExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/LocaleCollectionExtension.php index 87bb38cbe2..88d4602f23 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/LocaleCollectionExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/LocaleCollectionExtension.php @@ -22,7 +22,6 @@ use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Locale\Model\LocaleInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class LocaleCollectionExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/OrdersByChannelExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/OrdersByChannelExtension.php index 3faa30a56f..d92f0c1e82 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/OrdersByChannelExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/OrdersByChannelExtension.php @@ -24,7 +24,6 @@ use Sylius\Component\Core\Model\ShopUserInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Webmozart\Assert\Assert; -/** @experimental */ final class OrdersByChannelExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/OrdersByLoggedInUserExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/OrdersByLoggedInUserExtension.php index 1077cc4f16..f0c3263896 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/OrdersByLoggedInUserExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/OrdersByLoggedInUserExtension.php @@ -23,7 +23,6 @@ use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; -/** @experimental */ final class OrdersByLoggedInUserExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsByChannelAndLocaleCodeExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsByChannelAndLocaleCodeExtension.php index a94b74f336..7108993c84 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsByChannelAndLocaleCodeExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsByChannelAndLocaleCodeExtension.php @@ -21,7 +21,6 @@ use Sylius\Bundle\ApiBundle\Serializer\ContextKeys; use Sylius\Component\Core\Model\ProductInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class ProductsByChannelAndLocaleCodeExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsByTaxonExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsByTaxonExtension.php index 09580dd064..8a93ab86be 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsByTaxonExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsByTaxonExtension.php @@ -19,7 +19,6 @@ use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Component\Core\Model\ProductInterface; -/** @experimental */ final class ProductsByTaxonExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct( @@ -75,7 +74,10 @@ final class ProductsByTaxonExtension implements ContextAwareQueryCollectionExten sprintf('%s.taxon', $productTaxonAliasName), $taxonAliasName, 'WITH', - sprintf('%s.code IN (:%s)', $taxonAliasName, $taxonCodeParameterName), + $queryBuilder->expr()->andX( + $queryBuilder->expr()->in(sprintf('%s.code', $taxonAliasName), sprintf(':%s', $taxonCodeParameterName)), + $queryBuilder->expr()->eq(sprintf('%s.enabled', $taxonAliasName), 'true'), + ), ) ->orderBy(sprintf('%s.position', $productTaxonAliasName), 'ASC') ->setParameter($taxonCodeParameterName, $taxonCode) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsWithEnableFlagExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsWithEnableFlagExtension.php index ab3927e9c6..a34fc5aafe 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsWithEnableFlagExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/ProductsWithEnableFlagExtension.php @@ -19,7 +19,6 @@ use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Component\Core\Model\ProductInterface; -/** @experimental */ final class ProductsWithEnableFlagExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/RestrictingFilterEagerLoadingExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/RestrictingFilterEagerLoadingExtension.php index 81247e1e44..fd1562ec08 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/RestrictingFilterEagerLoadingExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/RestrictingFilterEagerLoadingExtension.php @@ -18,7 +18,6 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; /** - * @experimental * This class decorates api_platform.doctrine.orm.query_extension.filter_eager_loading. * It is a workaround for https://github.com/api-platform/core/issues/2253. */ diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/TaxonCollectionExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/TaxonCollectionExtension.php index 952cf1516c..0750702793 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/TaxonCollectionExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryCollectionExtension/TaxonCollectionExtension.php @@ -22,7 +22,6 @@ use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Taxonomy\Model\TaxonInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class TaxonCollectionExtension implements ContextAwareQueryCollectionExtensionInterface { public function __construct(private UserContextInterface $userContext) @@ -40,14 +39,14 @@ final class TaxonCollectionExtension implements ContextAwareQueryCollectionExten return; } - Assert::keyExists($context, ContextKeys::CHANNEL); - $channelMenuTaxon = $context[ContextKeys::CHANNEL]->getMenuTaxon(); - $user = $this->userContext->getUser(); if ($user instanceof AdminUserInterface && in_array('ROLE_API_ACCESS', $user->getRoles(), true)) { return; } + Assert::keyExists($context, ContextKeys::CHANNEL); + $channelMenuTaxon = $context[ContextKeys::CHANNEL]->getMenuTaxon(); + $enabledParameterName = $queryNameGenerator->generateParameterName('enabled'); $parentCodeParameterName = $queryNameGenerator->generateParameterName('parentCode'); @@ -55,7 +54,7 @@ final class TaxonCollectionExtension implements ContextAwareQueryCollectionExten $queryBuilder ->addSelect('child') ->innerJoin(sprintf('%s.parent', $rootAlias), 'parent') - ->leftJoin(sprintf('%s.children', $rootAlias), 'child') + ->leftJoin(sprintf('%s.children', $rootAlias), 'child', 'WITH', 'child.enabled = true') ->andWhere(sprintf('%s.enabled = :%s', $rootAlias, $enabledParameterName)) ->andWhere(sprintf('parent.code = :%s', $parentCodeParameterName)) ->addOrderBy(sprintf('%s.position', $rootAlias)) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/ExchangeRateExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/ExchangeRateExtension.php index 2cb2e1bf0b..e52044e151 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/ExchangeRateExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/ExchangeRateExtension.php @@ -23,7 +23,6 @@ use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Currency\Model\ExchangeRate; use Webmozart\Assert\Assert; -/** @experimental */ final class ExchangeRateExtension implements ContextAwareQueryCollectionExtensionInterface, QueryItemExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php new file mode 100644 index 0000000000..a7b3f1851a --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryExtension/OrderExtension.php @@ -0,0 +1,85 @@ + $orderStatesToFilterOut + */ + public function __construct( + private SectionProviderInterface $sectionProvider, + private array $orderStatesToFilterOut, + ) { + } + + /** + * @param array $context + */ + public function applyToCollection( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + string $operationName = null, + array $context = [], + ): void { + $this->filterOutOrders($queryBuilder, $queryNameGenerator, $resourceClass); + } + + /** + * @param array $context + * @param array $identifiers + */ + public function applyToItem( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + array $identifiers, + string $operationName = null, + array $context = [], + ): void { + $this->filterOutOrders($queryBuilder, $queryNameGenerator, $resourceClass); + } + + private function filterOutOrders( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ): void { + if (!is_a($resourceClass, OrderInterface::class, true)) { + return; + } + + if (!$this->sectionProvider->getSection() instanceof AdminApiSection) { + return; + } + + $stateParameter = $queryNameGenerator->generateParameterName('state'); + $rootAlias = $queryBuilder->getRootAliases()[0]; + + $queryBuilder + ->andWhere($queryBuilder->expr()->notIn(sprintf('%s.state', $rootAlias), sprintf(':%s', $stateParameter))) + ->setParameter($stateParameter, $this->orderStatesToFilterOut, ArrayParameterType::STRING) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/AddressItemExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/AddressItemExtension.php index 3ba91a9ab6..88658c3323 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/AddressItemExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/AddressItemExtension.php @@ -25,7 +25,6 @@ use Sylius\Component\Core\Model\ShopUserInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\Security\Core\User\UserInterface; -/** @experimental */ final class AddressItemExtension implements QueryItemExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductInProductAssociationItemExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductInProductAssociationItemExtension.php new file mode 100644 index 0000000000..06a1c0733a --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductInProductAssociationItemExtension.php @@ -0,0 +1,60 @@ +userContext->getUser(); + if ($user instanceof AdminUserInterface && in_array('ROLE_API_ACCESS', $user->getRoles(), true)) { + return; + } + + Assert::keyExists($context, ContextKeys::CHANNEL); + + $rootAlias = $queryBuilder->getRootAliases()[0]; + $enabled = $queryNameGenerator->generateParameterName('enabled'); + $channel = $queryNameGenerator->generateParameterName('channel'); + + $queryBuilder->addSelect('associatedProduct'); + $queryBuilder->leftJoin(sprintf('%s.associatedProducts', $rootAlias), 'associatedProduct', 'WITH', sprintf('associatedProduct.enabled = :%s', $enabled)); + $queryBuilder->innerJoin('associatedProduct.channels', 'channel', 'WITH', sprintf('channel = :%s', $channel)); + $queryBuilder->setParameter($enabled, true); + $queryBuilder->setParameter($channel, $context[ContextKeys::CHANNEL]); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductVariantItemExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductVariantItemExtension.php index 0346fb4cdf..5de7b72616 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductVariantItemExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/EnabledProductVariantItemExtension.php @@ -19,7 +19,6 @@ use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; use Sylius\Component\Core\Model\ProductVariantInterface; -/** @experimental */ final class EnabledProductVariantItemExtension implements QueryItemExtensionInterface { public function __construct(private UserContextInterface $userContext) diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php index 9041e9a088..cb12948f19 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderShopUserItemExtension.php @@ -22,11 +22,12 @@ use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Symfony\Component\HttpFoundation\Request; -/** @experimental */ final class OrderShopUserItemExtension implements QueryItemExtensionInterface { - public function __construct(private UserContextInterface $userContext) - { + public function __construct( + private UserContextInterface $userContext, + private array $nonFilteredCartAllowedOperations = [], + ) { } public function applyToItem( @@ -56,11 +57,7 @@ final class OrderShopUserItemExtension implements QueryItemExtensionInterface $httpRequestMethodType = $context[ContextKeys::HTTP_REQUEST_METHOD_TYPE]; - if ( - $operationName === 'shop_select_payment_method' || - $operationName === 'shop_account_change_payment_method' || - $httpRequestMethodType === Request::METHOD_GET - ) { + if ($httpRequestMethodType === Request::METHOD_GET || in_array($operationName, $this->nonFilteredCartAllowedOperations, true)) { return; } diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderVisitorItemExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderVisitorItemExtension.php index db91f10ab2..e77d4972b0 100644 --- a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderVisitorItemExtension.php +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/OrderVisitorItemExtension.php @@ -21,11 +21,12 @@ use Sylius\Bundle\ApiBundle\Serializer\ContextKeys; use Sylius\Component\Core\Model\OrderInterface; use Symfony\Component\HttpFoundation\Request; -/** @experimental */ final class OrderVisitorItemExtension implements QueryItemExtensionInterface { - public function __construct(private UserContextInterface $userContext) - { + public function __construct( + private UserContextInterface $userContext, + private array $nonFilteredCartAllowedOperations = [], + ) { } public function applyToItem( @@ -62,7 +63,7 @@ final class OrderVisitorItemExtension implements QueryItemExtensionInterface $httpRequestMethodType = $context[ContextKeys::HTTP_REQUEST_METHOD_TYPE]; - if ($httpRequestMethodType === Request::METHOD_GET || $operationName === 'shop_select_payment_method') { + if ($httpRequestMethodType === Request::METHOD_GET || in_array($operationName, $this->nonFilteredCartAllowedOperations, true)) { return; } diff --git a/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/TaxonItemExtension.php b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/TaxonItemExtension.php new file mode 100644 index 0000000000..38035b1899 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Doctrine/QueryItemExtension/TaxonItemExtension.php @@ -0,0 +1,54 @@ +userContext->getUser(); + if ($user !== null && in_array('ROLE_API_ACCESS', $user->getRoles(), true)) { + return; + } + + $rootAlias = $queryBuilder->getRootAliases()[0]; + $enabledParameter = $queryNameGenerator->generateParameterName('enabled'); + $childAlias = $queryNameGenerator->generateJoinAlias('child'); + + $queryBuilder->addSelect($childAlias); + $queryBuilder->leftJoin(sprintf('%s.children', $rootAlias), $childAlias, 'WITH', sprintf('%s.enabled = :%s', $childAlias, $enabledParameter)); + $queryBuilder->andWhere(sprintf('%s.enabled = :%s', $rootAlias, $enabledParameter)); + $queryBuilder->setParameter($enabledParameter, true); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/EventHandler/OrderCompletedHandler.php b/src/Sylius/Bundle/ApiBundle/EventHandler/OrderCompletedHandler.php index d33294ac81..f364fd229c 100644 --- a/src/Sylius/Bundle/ApiBundle/EventHandler/OrderCompletedHandler.php +++ b/src/Sylius/Bundle/ApiBundle/EventHandler/OrderCompletedHandler.php @@ -17,7 +17,6 @@ use Sylius\Bundle\ApiBundle\Command\Checkout\SendOrderConfirmation; use Sylius\Bundle\ApiBundle\Event\OrderCompleted; use Symfony\Component\Messenger\MessageBusInterface; -/** @experimental */ final class OrderCompletedHandler { public function __construct(private MessageBusInterface $commandBus) diff --git a/src/Sylius/Bundle/ApiBundle/EventListener/AdminAuthenticationSuccessListener.php b/src/Sylius/Bundle/ApiBundle/EventListener/AdminAuthenticationSuccessListener.php new file mode 100644 index 0000000000..c9bed116e5 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/EventListener/AdminAuthenticationSuccessListener.php @@ -0,0 +1,39 @@ +getData(); + $adminUser = $event->getUser(); + + if (!$adminUser instanceof AdminUserInterface) { + return; + } + + $data['adminUser'] = $this->iriConverter->getIriFromResource($adminUser); + + $event->setData($data); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/EventListener/AuthenticationSuccessListener.php b/src/Sylius/Bundle/ApiBundle/EventListener/AuthenticationSuccessListener.php index 124ae37494..632449e420 100644 --- a/src/Sylius/Bundle/ApiBundle/EventListener/AuthenticationSuccessListener.php +++ b/src/Sylius/Bundle/ApiBundle/EventListener/AuthenticationSuccessListener.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\EventListener; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ShopUserInterface; @@ -36,7 +36,7 @@ final class AuthenticationSuccessListener /** @var CustomerInterface $customer */ $customer = $user->getCustomer(); - $data['customer'] = $this->iriConverter->getIriFromItem($customer); + $data['customer'] = $this->iriConverter->getIriFromResource($customer); $event->setData($data); } diff --git a/src/Sylius/Bundle/ApiBundle/EventSubscriber/AttributeEventSubscriber.php b/src/Sylius/Bundle/ApiBundle/EventSubscriber/AttributeEventSubscriber.php new file mode 100644 index 0000000000..6a61015910 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/EventSubscriber/AttributeEventSubscriber.php @@ -0,0 +1,68 @@ + ['assignStorageType', EventPriorities::PRE_VALIDATE], + ]; + } + + public function assignStorageType(ViewEvent $event): void + { + $attribute = $event->getControllerResult(); + $method = $event->getRequest()->getMethod(); + + if ( + !$attribute instanceof AttributeInterface || + !in_array($method, [Request::METHOD_POST, Request::METHOD_PUT], true) + ) { + return; + } + + if (null === $attribute->getType() || '' === $attribute->getType()) { + return; + } + + if ( + null !== $attribute->getStorageType() || + !$this->attributeTypeRegistry->has($attribute->getType()) + ) { + return; + } + + /** @var AttributeTypeInterface $attributeType */ + $attributeType = $this->attributeTypeRegistry->get($attribute->getType()); + + $attribute->setStorageType($attributeType->getStorageType()); + + $event->setControllerResult($attribute); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/EventSubscriber/KernelRequestEventSubscriber.php b/src/Sylius/Bundle/ApiBundle/EventSubscriber/KernelRequestEventSubscriber.php index a35fa58e4f..b7f2f7f172 100644 --- a/src/Sylius/Bundle/ApiBundle/EventSubscriber/KernelRequestEventSubscriber.php +++ b/src/Sylius/Bundle/ApiBundle/EventSubscriber/KernelRequestEventSubscriber.php @@ -19,7 +19,6 @@ use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\KernelEvents; -/** @experimental */ final class KernelRequestEventSubscriber implements EventSubscriberInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/EventSubscriber/ProductSlugEventSubscriber.php b/src/Sylius/Bundle/ApiBundle/EventSubscriber/ProductSlugEventSubscriber.php index b18ec67505..22eda094b5 100644 --- a/src/Sylius/Bundle/ApiBundle/EventSubscriber/ProductSlugEventSubscriber.php +++ b/src/Sylius/Bundle/ApiBundle/EventSubscriber/ProductSlugEventSubscriber.php @@ -22,7 +22,6 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ViewEvent; use Symfony\Component\HttpKernel\KernelEvents; -/** @experimental */ final class ProductSlugEventSubscriber implements EventSubscriberInterface { public function __construct(private SlugGeneratorInterface $slugGenerator) diff --git a/src/Sylius/Bundle/ApiBundle/EventSubscriber/TaxonDeletionEventSubscriber.php b/src/Sylius/Bundle/ApiBundle/EventSubscriber/TaxonDeletionEventSubscriber.php new file mode 100644 index 0000000000..821d5d68e5 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/EventSubscriber/TaxonDeletionEventSubscriber.php @@ -0,0 +1,74 @@ + [ + ['protectFromRemovingMenuTaxon', EventPriorities::PRE_WRITE], + ['protectFromRemovingTaxonInUseByPromotionRule', EventPriorities::PRE_WRITE], + ], + ]; + } + + public function protectFromRemovingMenuTaxon(ViewEvent $event): void + { + $taxon = $event->getControllerResult(); + $method = $event->getRequest()->getMethod(); + + if (!$taxon instanceof TaxonInterface || $method !== Request::METHOD_DELETE) { + return; + } + + $channel = $this->channelRepository->findOneBy(['menuTaxon' => $taxon]); + + if ($channel !== null) { + throw new CannotRemoveMenuTaxonException($taxon->getCode()); + } + } + + public function protectFromRemovingTaxonInUseByPromotionRule(ViewEvent $event): void + { + $taxon = $event->getControllerResult(); + $method = $event->getRequest()->getMethod(); + + if (!$taxon instanceof TaxonInterface || $method !== Request::METHOD_DELETE) { + return; + } + + if ($this->taxonInPromotionRuleChecker->isInUse($taxon)) { + throw new TaxonCannotBeRemoved('Cannot delete a taxon that is in use by a promotion rule.'); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/EventSubscriber/TaxonSlugEventSubscriber.php b/src/Sylius/Bundle/ApiBundle/EventSubscriber/TaxonSlugEventSubscriber.php new file mode 100644 index 0000000000..7abb0c4152 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/EventSubscriber/TaxonSlugEventSubscriber.php @@ -0,0 +1,65 @@ + ['generateSlug', EventPriorities::PRE_VALIDATE], + ]; + } + + public function generateSlug(ViewEvent $event): void + { + $taxon = $event->getControllerResult(); + $method = $event->getRequest()->getMethod(); + + if ( + !$taxon instanceof TaxonInterface || + !in_array($method, [Request::METHOD_POST, Request::METHOD_PUT], true) + ) { + return; + } + + /** @var TaxonTranslationInterface $translation */ + foreach ($taxon->getTranslations() as $translation) { + if ($translation->getSlug() !== null && $translation->getSlug() !== '') { + continue; + } + + if ($translation->getName() === null || $translation->getName() === '') { + continue; + } + + $translation->setSlug($this->taxonSlugGenerator->generate($taxon, $translation->getLocale())); + } + + $event->setControllerResult($taxon); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Exception/CannotRemoveCurrentlyLoggedInUser.php b/src/Sylius/Bundle/ApiBundle/Exception/CannotRemoveCurrentlyLoggedInUser.php index 8d15093bdd..b3b6c55503 100644 --- a/src/Sylius/Bundle/ApiBundle/Exception/CannotRemoveCurrentlyLoggedInUser.php +++ b/src/Sylius/Bundle/ApiBundle/Exception/CannotRemoveCurrentlyLoggedInUser.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Exception; -/** @experimental */ final class CannotRemoveCurrentlyLoggedInUser extends \RuntimeException { public function __construct() diff --git a/src/Sylius/Bundle/ApiBundle/Exception/CannotRemoveMenuTaxonException.php b/src/Sylius/Bundle/ApiBundle/Exception/CannotRemoveMenuTaxonException.php new file mode 100644 index 0000000000..76124ee23f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Exception/CannotRemoveMenuTaxonException.php @@ -0,0 +1,22 @@ + $headers */ + public function __construct( + string $message = 'No file was uploaded.', + \Throwable $previous = null, + int $code = 0, + array $headers = [], + ) { + parent::__construct($message, $previous, $code, $headers); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Exception/OrderItemNotFoundException.php b/src/Sylius/Bundle/ApiBundle/Exception/OrderItemNotFoundException.php index 0359bc84c1..6e35f610cc 100644 --- a/src/Sylius/Bundle/ApiBundle/Exception/OrderItemNotFoundException.php +++ b/src/Sylius/Bundle/ApiBundle/Exception/OrderItemNotFoundException.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Exception; -/** @experimental */ final class OrderItemNotFoundException extends \RuntimeException { public function __construct() diff --git a/src/Sylius/Bundle/ApiBundle/Exception/OrderNoLongerEligibleForPromotion.php b/src/Sylius/Bundle/ApiBundle/Exception/OrderNoLongerEligibleForPromotion.php index 306fec0448..ca356629d4 100644 --- a/src/Sylius/Bundle/ApiBundle/Exception/OrderNoLongerEligibleForPromotion.php +++ b/src/Sylius/Bundle/ApiBundle/Exception/OrderNoLongerEligibleForPromotion.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Exception; -/** @experimental */ final class OrderNoLongerEligibleForPromotion extends \RuntimeException { public function __construct(string $promotionName) diff --git a/src/Sylius/Bundle/ApiBundle/Exception/PaymentMethodCannotBeChangedException.php b/src/Sylius/Bundle/ApiBundle/Exception/PaymentMethodCannotBeChangedException.php new file mode 100644 index 0000000000..c9e0bf31af --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Exception/PaymentMethodCannotBeChangedException.php @@ -0,0 +1,22 @@ + $headers */ + public function __construct( + string $promotionCode, + string $message = 'Promotion with the "%s" code not found.', + \Throwable $previous = null, + int $code = 0, + array $headers = [], + ) { + parent::__construct( + sprintf($message, $promotionCode), + $previous, + $code, + $headers, + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Exception/ProvinceCannotBeRemoved.php b/src/Sylius/Bundle/ApiBundle/Exception/ProvinceCannotBeRemoved.php index ac0d555125..6c94fa4c54 100644 --- a/src/Sylius/Bundle/ApiBundle/Exception/ProvinceCannotBeRemoved.php +++ b/src/Sylius/Bundle/ApiBundle/Exception/ProvinceCannotBeRemoved.php @@ -13,11 +13,13 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Exception; -/** @experimental */ final class ProvinceCannotBeRemoved extends \RuntimeException { - public function __construct() - { - parent::__construct('Cannot delete, the province is in use.'); + public function __construct( + string $message = 'Cannot delete, the province is in use.', + int $code = 0, + \Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); } } diff --git a/src/Sylius/Bundle/ApiBundle/Exception/ShippingMethodCannotBeRemoved.php b/src/Sylius/Bundle/ApiBundle/Exception/ShippingMethodCannotBeRemoved.php index 16167bb6bd..5638ca45c0 100644 --- a/src/Sylius/Bundle/ApiBundle/Exception/ShippingMethodCannotBeRemoved.php +++ b/src/Sylius/Bundle/ApiBundle/Exception/ShippingMethodCannotBeRemoved.php @@ -13,11 +13,13 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Exception; -/** @experimental */ final class ShippingMethodCannotBeRemoved extends \RuntimeException { - public function __construct() - { - parent::__construct('Cannot delete, the shipping method is in use.'); + public function __construct( + string $message = 'Cannot delete, the shipping method is in use.', + int $code = 0, + \Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); } } diff --git a/src/Sylius/Bundle/ApiBundle/Exception/StateMachineTransitionFailedException.php b/src/Sylius/Bundle/ApiBundle/Exception/StateMachineTransitionFailedException.php new file mode 100644 index 0000000000..3658d8d383 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Exception/StateMachineTransitionFailedException.php @@ -0,0 +1,25 @@ + $headers */ + public function __construct( + string $message = 'Taxon not found.', + \Throwable $previous = null, + int $code = 0, + array $headers = [], + ) { + parent::__construct($message, $previous, $code, $headers); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Exception/TranslationInDefaultLocaleCannotBeRemoved.php b/src/Sylius/Bundle/ApiBundle/Exception/TranslationInDefaultLocaleCannotBeRemoved.php new file mode 100644 index 0000000000..ce518b8219 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Exception/TranslationInDefaultLocaleCannotBeRemoved.php @@ -0,0 +1,25 @@ + $headers */ + public function __construct( + string $message = 'User not found.', + \Throwable $previous = null, + int $code = 0, + array $headers = [], + ) { + parent::__construct($message, $previous, $code, $headers); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Exception/ZoneCannotBeRemoved.php b/src/Sylius/Bundle/ApiBundle/Exception/ZoneCannotBeRemoved.php index 2084ea9ed9..5d08fda1e8 100644 --- a/src/Sylius/Bundle/ApiBundle/Exception/ZoneCannotBeRemoved.php +++ b/src/Sylius/Bundle/ApiBundle/Exception/ZoneCannotBeRemoved.php @@ -13,11 +13,13 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Exception; -/** @experimental */ final class ZoneCannotBeRemoved extends \RuntimeException { - public function __construct() - { - parent::__construct('Cannot delete, the zone is in use.'); + public function __construct( + string $message = 'Cannot delete, the zone is in use.', + int $code = 0, + \Throwable $previous = null, + ) { + parent::__construct($message, $code, $previous); } } diff --git a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/CatalogPromotionChannelFilter.php b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ChannelsAwareChannelFilter.php similarity index 90% rename from src/Sylius/Bundle/ApiBundle/Filter/Doctrine/CatalogPromotionChannelFilter.php rename to src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ChannelsAwareChannelFilter.php index 76d812fa2d..c73c9ebe2c 100644 --- a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/CatalogPromotionChannelFilter.php +++ b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ChannelsAwareChannelFilter.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Filter\Doctrine; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; @@ -22,8 +22,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -/** @experimental */ -final class CatalogPromotionChannelFilter extends AbstractContextAwareFilter +final class ChannelsAwareChannelFilter extends AbstractContextAwareFilter { public function __construct( private IriConverterInterface $iriConverter, @@ -48,7 +47,7 @@ final class CatalogPromotionChannelFilter extends AbstractContextAwareFilter return; } - $channel = $this->iriConverter->getItemFromIri($value); + $channel = $this->iriConverter->getResourceFromIri($value); $parameterName = $queryNameGenerator->generateParameterName($property); $rootAlias = $queryBuilder->getRootAliases()[0]; $queryBuilder diff --git a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ExchangeRateFilter.php b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ExchangeRateFilter.php index 42171463fb..9cf0eb2bcc 100644 --- a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ExchangeRateFilter.php +++ b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ExchangeRateFilter.php @@ -17,7 +17,6 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; -/** @experimental */ final class ExchangeRateFilter extends AbstractContextAwareFilter { protected function filterProperty( diff --git a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductPriceOrderFilter.php b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductPriceOrderFilter.php index cc4499a6ee..636f8b2b51 100644 --- a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductPriceOrderFilter.php +++ b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductPriceOrderFilter.php @@ -21,7 +21,6 @@ use Sylius\Bundle\ApiBundle\Serializer\ContextKeys; use Sylius\Component\Core\Model\ProductInterface; use Webmozart\Assert\Assert; -/** @experimental */ final class ProductPriceOrderFilter extends AbstractContextAwareFilter { protected function filterProperty( diff --git a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductVariantCatalogPromotionFilter.php b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductVariantCatalogPromotionFilter.php index d0208f9256..0d05e2c25b 100644 --- a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductVariantCatalogPromotionFilter.php +++ b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductVariantCatalogPromotionFilter.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Filter\Doctrine; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\Query\Expr\Join; @@ -23,7 +23,6 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -/** @experimental */ final class ProductVariantCatalogPromotionFilter extends AbstractContextAwareFilter { public function __construct( @@ -49,7 +48,7 @@ final class ProductVariantCatalogPromotionFilter extends AbstractContextAwareFil return; } - $catalogPromotion = $this->iriConverter->getItemFromIri($value); + $catalogPromotion = $this->iriConverter->getResourceFromIri($value); $parameterName = $queryNameGenerator->generateParameterName($property); $channelPricingJoinAlias = $queryNameGenerator->generateJoinAlias('channelPricing'); diff --git a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductVariantOptionValueFilter.php b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductVariantOptionValueFilter.php index 1a1298dfc1..18141fa34c 100644 --- a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductVariantOptionValueFilter.php +++ b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/ProductVariantOptionValueFilter.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Filter\Doctrine; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; @@ -22,7 +22,6 @@ use Psr\Log\LoggerInterface; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; -/** @experimental */ final class ProductVariantOptionValueFilter extends AbstractContextAwareFilter { public function __construct( @@ -51,7 +50,7 @@ final class ProductVariantOptionValueFilter extends AbstractContextAwareFilter $value = (array) $value; foreach ($value as $optionValueIri) { - $optionValue = $this->iriConverter->getItemFromIri($optionValueIri); + $optionValue = $this->iriConverter->getResourceFromIri($optionValueIri); $parameterName = $queryNameGenerator->generateParameterName($property); $rootAlias = $queryBuilder->getRootAliases()[0]; diff --git a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/PromotionCouponPromotionFilter.php b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/PromotionCouponPromotionFilter.php new file mode 100644 index 0000000000..a9d5d2c1bb --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/PromotionCouponPromotionFilter.php @@ -0,0 +1,86 @@ + $properties */ + public function __construct( + private IriConverterInterface $iriConverter, + ManagerRegistry $managerRegistry, + ?RequestStack $requestStack = null, + LoggerInterface $logger = null, + array $properties = null, + NameConverterInterface $nameConverter = null, + ) { + parent::__construct($managerRegistry, $requestStack, $logger, $properties, $nameConverter); + } + + protected function filterProperty( + string $property, + $value, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + string $operationName = null, + ): void { + if (self::PROPERTY !== $property) { + return; + } + + $promotion = $this->iriConverter->getResourceFromIri($value); + + $parameterName = $queryNameGenerator->generateParameterName(':promotion'); + $promotionJoinAlias = $queryNameGenerator->generateJoinAlias('promotion'); + $rootAlias = $queryBuilder->getRootAliases()[0]; + + $queryBuilder + ->innerJoin( + sprintf('%s.promotion', $rootAlias), + $promotionJoinAlias, + Join::WITH, + $queryBuilder->expr()->eq(sprintf('%s.id', $promotionJoinAlias), $parameterName), + ) + ->setParameter($parameterName, $promotion) + ; + } + + /** @return array */ + public function getDescription(string $resourceClass): array + { + return [ + self::PROPERTY => [ + 'type' => 'string', + 'required' => false, + 'property' => null, + 'description' => 'Get a collection of promotion coupons for promotion', + 'schema' => [ + 'type' => 'string', + ], + ], + ]; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TaxonFilter.php b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TaxonFilter.php index de47dad0e5..ba405de58c 100644 --- a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TaxonFilter.php +++ b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TaxonFilter.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Filter\Doctrine; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; @@ -48,7 +48,7 @@ final class TaxonFilter extends AbstractContextAwareFilter try { /** @var TaxonInterface $taxon */ - $taxon = $this->iriConverter->getItemFromIri($value); + $taxon = $this->iriConverter->getResourceFromIri($value); $taxonRoot = $taxon->getRoot(); } catch (InvalidArgumentException|ItemNotFoundException $argumentException) { $taxonRoot = null; diff --git a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TranslationOrderNameAndLocaleFilter.php b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TranslationOrderNameAndLocaleFilter.php index ffa1897e19..e0edc73296 100644 --- a/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TranslationOrderNameAndLocaleFilter.php +++ b/src/Sylius/Bundle/ApiBundle/Filter/Doctrine/TranslationOrderNameAndLocaleFilter.php @@ -18,7 +18,6 @@ use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; -/** @experimental */ final class TranslationOrderNameAndLocaleFilter extends AbstractContextAwareFilter { protected function filterProperty( @@ -42,7 +41,7 @@ final class TranslationOrderNameAndLocaleFilter extends AbstractContextAwareFilt $queryBuilder ->addSelect('translation') - ->innerJoin( + ->leftJoin( sprintf('%s.translations', $rootAlias), 'translation', 'WITH', diff --git a/src/Sylius/Bundle/ApiBundle/Modifier/OrderAddressModifier.php b/src/Sylius/Bundle/ApiBundle/Modifier/OrderAddressModifier.php index e555b699b7..49e94d59b8 100644 --- a/src/Sylius/Bundle/ApiBundle/Modifier/OrderAddressModifier.php +++ b/src/Sylius/Bundle/ApiBundle/Modifier/OrderAddressModifier.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Modifier; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Bundle\ApiBundle\Mapper\AddressMapperInterface; use Sylius\Component\Core\Model\AddressInterface; use Sylius\Component\Core\Model\OrderInterface; @@ -23,9 +25,20 @@ use Webmozart\Assert\Assert; final class OrderAddressModifier implements OrderAddressModifierInterface { public function __construct( - private StateMachineFactoryInterface $stateMachineFactory, + private StateMachineFactoryInterface|StateMachineInterface $stateMachineFactory, private AddressMapperInterface $addressMapper, ) { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + trigger_deprecation( + 'sylius/api-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the first argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + StateMachineFactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function modify( @@ -33,10 +46,9 @@ final class OrderAddressModifier implements OrderAddressModifierInterface ?AddressInterface $billingAddress, ?AddressInterface $shippingAddress = null, ): OrderInterface { - $stateMachine = $this->stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH); - + $stateMachine = $this->getStateMachine(); Assert::true( - $stateMachine->can(OrderCheckoutTransitions::TRANSITION_ADDRESS), + $stateMachine->can($order, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_ADDRESS), sprintf('Order with %s token cannot be addressed.', $order->getTokenValue()), ); @@ -59,7 +71,7 @@ final class OrderAddressModifier implements OrderAddressModifierInterface $this->setBillingAddress($order, $oldBillingAddress, $billingAddress); $this->setShippingAddress($order, $oldShippingAddress, $shippingAddress); - $stateMachine->apply(OrderCheckoutTransitions::TRANSITION_ADDRESS); + $stateMachine->apply($order, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_ADDRESS); return $order; } @@ -91,4 +103,13 @@ final class OrderAddressModifier implements OrderAddressModifierInterface $order->setShippingAddress($shippingAddress); } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } } diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AcceptLanguageHeaderDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AcceptLanguageHeaderDocumentationModifier.php new file mode 100644 index 0000000000..c64115785b --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AcceptLanguageHeaderDocumentationModifier.php @@ -0,0 +1,72 @@ + $localeRepository + */ + public function __construct(private RepositoryInterface $localeRepository) + { + } + + public function modify(OpenApi $docs): OpenApi + { + $acceptLanguageHeaderParameter = new Parameter( + name: 'Accept-Language', + in: 'header', + description: 'Locales in this enum are all locales defined in the shop and only enabled ones will work in the given channel in the shop.', + required: false, + schema: [ + 'type' => 'string', + 'enum' => array_map( + fn (LocaleInterface $locale): string => $locale->getCode(), + $this->localeRepository->findAll(), + ), + ], + ); + + $pathItems = []; + + /** @var PathItem $pathItem */ + foreach ($docs->getPaths()->getPaths() as $path => $pathItem) { + foreach (PathItem::$methods as $method) { + $operation = $pathItem->{'get' . ucfirst($method)}(); + + if (null === $operation) { + continue; + } + + $parameters = $operation->getParameters(); + $parameters[] = $acceptLanguageHeaderParameter; + + $operation = $operation->withParameters($parameters); + $pathItems[$path] = $pathItem->{'with' . ucfirst($method)}($operation); + } + } + + foreach ($pathItems as $path => $pathItem) { + $docs->getPaths()->addPath($path, $pathItem); + } + + return $docs; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AddressLogEntryDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AddressLogEntryDocumentationModifier.php new file mode 100644 index 0000000000..443409945d --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AddressLogEntryDocumentationModifier.php @@ -0,0 +1,95 @@ +getComponents(); + $schemas = $components->getSchemas(); + + $schemas['Address-admin.address.log_entry.read'] = [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'action' => [ + 'readOnly' => true, + 'type' => 'string', + ], + 'version' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'data' => [ + 'readOnly' => true, + 'type' => 'object', + ], + 'logged_at' => [ + 'readOnly' => true, + 'type' => 'string', + 'format' => 'date-time', + ], + ], + ], + ]; + + $schemas['Address.jsonld-admin.address.log_entry.read'] = [ + 'type' => 'object', + 'properties' => [ + 'hydra:member' => [ + 'readOnly' => true, + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + '@type' => [ + 'readOnly' => true, + 'type' => 'string', + ], + 'action' => [ + 'readOnly' => true, + 'type' => 'string', + ], + 'version' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + 'data' => [ + 'readOnly' => true, + 'type' => 'object', + ], + 'logged_at' => [ + 'readOnly' => true, + 'type' => 'string', + 'format' => 'date-time', + ], + ], + ], + ], + 'hydra:totalItems' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + ], + ]; + + $components = $components->withSchemas($schemas); + + return $docs->withComponents($components); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AdminAuthenticationTokenDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AdminAuthenticationTokenDocumentationModifier.php new file mode 100644 index 0000000000..413af1adeb --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AdminAuthenticationTokenDocumentationModifier.php @@ -0,0 +1,99 @@ +getComponents(); + $schemas = $components->getSchemas(); + + $schemas['AdminUserToken'] = [ + 'type' => 'object', + 'properties' => [ + 'token' => [ + 'type' => 'string', + 'readOnly' => true, + ], + 'adminUser' => [ + 'type' => 'string', + 'readOnly' => true, + ], + ], + ]; + + $schemas['AdminUserCredentials'] = [ + 'type' => 'object', + 'properties' => [ + 'email' => [ + 'type' => 'string', + 'example' => 'api@example.com', + ], + 'password' => [ + 'type' => 'string', + 'example' => 'sylius-api', + ], + ], + ]; + + $components = $components->withSchemas($schemas); + $docs = $docs->withComponents($components); + + $docs->getPaths()->addPath( + $this->apiRoute . '/admin/authentication-token', + new PathItem( + post: new Operation( + operationId: 'postCredentialsItem', + tags: ['AdminUserToken'], + responses: [ + Response::HTTP_OK => [ + 'description' => 'Get JWT token', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/AdminUserToken', + ], + ], + ], + ], + ], + summary: 'Get JWT token to login.', + requestBody: new RequestBody( + description: 'Create new JWT Token', + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/AdminUserCredentials', + ], + ], + ]), + ), + ), + ), + ); + + return $docs; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AttributeTypeDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AttributeTypeDocumentationModifier.php new file mode 100644 index 0000000000..94408e6bab --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/AttributeTypeDocumentationModifier.php @@ -0,0 +1,80 @@ +getComponents(); + $schemas = $components->getSchemas(); + if (null === $schemas) { + return $docs; + } + + $schemas = $this->updateAttributeTypesSchema($schemas); + + return $docs->withComponents( + $components->withSchemas($schemas), + ); + } + + /** + * @param \ArrayObject $schemas + * + * @return \ArrayObject + */ + private function updateAttributeTypesSchema(\ArrayObject $schemas): \ArrayObject + { + $attributeTypes = $this->getAttributeTypes(); + + $schemasToBeUpdated = [ + 'ProductAttribute.admin.product_attribute.read', + 'ProductAttribute.admin.product_attribute.create', + 'ProductAttribute.jsonld-admin.product_attribute.read', + 'ProductAttribute.jsonld-admin.product_attribute.create', + ]; + + foreach ($schemasToBeUpdated as $schemaToBeUpdated) { + $schemas[$schemaToBeUpdated]['properties']['type'] = [ + 'type' => 'string', + 'enum' => $attributeTypes, + ]; + } + + return $schemas; + } + + /** @return array */ + private function getAttributeTypes(): array + { + $attributeTypes = []; + + /** @var AttributeTypeInterface $attributeType */ + foreach ($this->attributeTypeRegistry->all() as $attributeType) { + $attributeTypes[] = $attributeType->getType(); + } + + return $attributeTypes; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/CustomerDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/CustomerDocumentationModifier.php new file mode 100644 index 0000000000..08bfece547 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/CustomerDocumentationModifier.php @@ -0,0 +1,121 @@ +updateVerifiedPropertyType($docs); + $docs = $this->updateCustomerStatisticsExampleResponse($docs); + + return $docs; + } + + private function updateVerifiedPropertyType(OpenApi $docs): OpenApi + { + $components = $docs->getComponents(); + $schemas = $components->getSchemas(); + + $schemas['ShopUser.jsonld-admin.customer.create']['properties']['verified'] = [ + 'type' => 'boolean', + 'default' => false, + 'example' => false, + ]; + + $schemas['ShopUser.jsonld-admin.customer.update']['properties']['verified'] = [ + 'type' => 'boolean', + 'default' => false, + 'example' => false, + ]; + + return $docs->withComponents($components->withSchemas($schemas)); + } + + private function updateCustomerStatisticsExampleResponse(OpenApi $docs): OpenApi + { + $components = $docs->getComponents(); + $schemas = $components->getSchemas(); + + $schemas['Customer-admin.customer.statistics.read'] = [ + 'type' => 'object', + 'properties' => [ + 'perChannelsStatistics' => [ + 'readOnly' => true, + 'type' => 'array', + 'items' => [ + 'type' => 'string', + ], + ], + 'allOrdersCount' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + ], + ]; + + $schemas['Customer.jsonld-admin.customer.statistics.read'] = [ + 'type' => 'object', + 'properties' => [ + '@context' => [ + 'readOnly' => true, + 'oneOf' => [ + [ + 'type' => 'string', + ], + [ + 'type' => 'object', + 'properties' => [ + '@vocab' => [ + 'type' => 'string', + ], + 'hydra' => [ + 'type' => 'string', + 'enum' => ['http://www.w3.org/ns/hydra/core#'], + ], + ], + 'required' => ['@vocab', 'hydra'], + 'additionalProperties' => true, + ], + ], + ], + '@id' => [ + 'readOnly' => true, + 'type' => 'string', + ], + '@type' => [ + 'readOnly' => true, + 'type' => 'string', + ], + 'perChannelsStatistics' => [ + 'readOnly' => true, + 'type' => 'array', + 'items' => [ + 'type' => 'string', + ], + ], + 'allOrdersCount' => [ + 'readOnly' => true, + 'type' => 'integer', + ], + ], + ]; + + $components = $components->withSchemas($schemas); + + return $docs->withComponents($components); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/DocumentationModifierInterface.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/DocumentationModifierInterface.php new file mode 100644 index 0000000000..f4a6953f94 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/DocumentationModifierInterface.php @@ -0,0 +1,21 @@ +getPaths(); + + $path = sprintf(self::PATH, $this->apiRoute); + $pathItem = $paths->getPath($path); + $operation = $pathItem->getGet(); + + $parameters = $operation->getParameters(); + $parameters[] = new Parameter( + name: 'type', + in: 'query', + description: 'Type of adjustments you want to get', + schema: [ + 'type' => 'string', + 'enum' => call_user_func([$this->adjustmentResourceClass, 'getAdjustmentTypeChoices']), + 'nullable' => true, + 'default' => null, + ], + ); + + $operation = $operation->withParameters($parameters); + $pathItem = $pathItem->withGet($operation); + $paths->addPath($path, $pathItem); + + return $docs->withPaths($paths); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/PathHiderDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/PathHiderDocumentationModifier.php new file mode 100644 index 0000000000..77efa2a736 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/PathHiderDocumentationModifier.php @@ -0,0 +1,48 @@ + $pathItems */ + $pathItems = $docs->getPaths()->getPaths(); + + foreach ($this->apiRoutes as $apiRoute) { + if (array_key_exists($apiRoute, $pathItems)) { + unset($pathItems[$apiRoute]); + } + } + + $paths = new Paths(); + + foreach ($pathItems as $path => $pathItem) { + $paths->addPath($path, $pathItem); + } + + return $docs->withPaths($paths); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductDocumentationModifier.php new file mode 100644 index 0000000000..6b1e3ee9d8 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductDocumentationModifier.php @@ -0,0 +1,36 @@ +getComponents(); + $schemas = $components->getSchemas(); + + $schemas['Product.jsonld-shop.product.read']['properties']['defaultVariant'] = [ + 'type' => 'string', + 'format' => 'iri-reference', + 'nullable' => true, + 'readOnly' => true, + ]; + + return $docs->withComponents( + $components->withSchemas($schemas), + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductImageDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductImageDocumentationModifier.php new file mode 100644 index 0000000000..ef8c9931fa --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductImageDocumentationModifier.php @@ -0,0 +1,54 @@ +filterProvider->provideShopFilters()); + + $path = sprintf('%s/shop/product-images/{id}', $this->apiRoute); + + $paths = $docs->getPaths(); + $pathItem = $paths->getPath($path); + $operation = $pathItem->getGet(); + /** @var Parameter[] $parameters */ + $parameters = $operation->getParameters(); + + foreach ($parameters as &$parameter) { + if ($parameter->getIn() === 'query' && $parameter->getName() === 'filter') { + $schema = $parameter->getSchema(); + $schema['enum'] = $enums; + $parameter = $parameter->withSchema($schema); + } + } + + $operation = $operation->withParameters($parameters); + $pathItem = $pathItem->withGet($operation); + $paths->addPath($path, $pathItem); + + return $docs; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductReviewDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductReviewDocumentationModifier.php new file mode 100644 index 0000000000..8c3844596e --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductReviewDocumentationModifier.php @@ -0,0 +1,61 @@ +getPaths(); + + $path = sprintf(self::PATH, $this->apiRoute); + $pathItem = $paths->getPath($path); + $operation = $pathItem->getGet(); + + $parameters = $operation->getParameters(); + $parameters = array_filter( + $parameters, + fn (Parameter $parameter) => $parameter->getName() !== 'status' && $parameter->getName() !== 'status[]', + ); + $parameters[] = new Parameter( + name: 'status', + in: 'query', + description: 'Status of product reviews you want to get', + schema: [ + 'type' => 'string', + 'enum' => [ReviewInterface::STATUS_NEW, ReviewInterface::STATUS_ACCEPTED, ReviewInterface::STATUS_REJECTED], + 'nullable' => true, + 'default' => null, + ], + ); + $parameters = array_values($parameters); + + $operation = $operation->withParameters($parameters); + $pathItem = $pathItem->withGet($operation); + $paths->addPath($path, $pathItem); + + return $docs->withPaths($paths); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductSlugDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductSlugDocumentationModifier.php new file mode 100644 index 0000000000..7c9f5bbe92 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductSlugDocumentationModifier.php @@ -0,0 +1,47 @@ +apiRoute); + + $paths = $docs->getPaths(); + $pathItem = $paths->getPath($path); + $operation = $pathItem->getGet(); + /** @var Parameter[] $parameters */ + $parameters = $operation->getParameters(); + + foreach ($parameters as $key => $parameter) { + if ($parameter->getName() === 'code') { + unset($parameters[$key]); + } + } + + $operation = $operation->withParameters(array_values($parameters)); + $pathItem = $pathItem->withGet($operation); + $paths->addPath($path, $pathItem); + + return $docs; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductVariantDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductVariantDocumentationModifier.php new file mode 100644 index 0000000000..4063966cc6 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ProductVariantDocumentationModifier.php @@ -0,0 +1,46 @@ +getComponents(); + $schemas = $components->getSchemas(); + + $schemas['ProductVariant.jsonld-shop.product_variant.read']['properties']['price'] = [ + 'type' => 'integer', + 'readOnly' => true, + 'default' => 0, + ]; + + $schemas['ProductVariant.jsonld-shop.product_variant.read']['properties']['inStock'] = [ + 'type' => 'boolean', + 'readOnly' => true, + ]; + + $schemas['ProductVariant.jsonld-shop.product_variant.read']['properties']['originalPrice'] = [ + 'type' => 'integer', + 'readOnly' => true, + 'default' => 0, + ]; + + return $docs->withComponents( + $components->withSchemas($schemas), + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/PromotionDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/PromotionDocumentationModifier.php new file mode 100644 index 0000000000..8764674839 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/PromotionDocumentationModifier.php @@ -0,0 +1,64 @@ +getPaths(); + + $this->addDescription($paths, sprintf('%s%s', $this->apiRoute, self::ROUTE_ADMIN_PROMOTIONS), 'Post'); + $this->addDescription($paths, sprintf('%s%s', $this->apiRoute, self::ROUTE_ADMIN_PROMOTION), 'Put'); + + return $docs->withPaths($paths); + } + + public function addDescription(Paths $paths, string $path, string $method): void + { + $pathItem = $paths->getPath($path); + $methodGet = sprintf('get%s', $method); + $operation = $pathItem->$methodGet(); + + $description = sprintf( + "%s\n\n Allowed rule types: `%s` \n\n Allowed action types: `%s`", + $operation->getDescription(), + implode('`, `', array_keys($this->ruleTypes)), + implode('`, `', array_keys($this->actionTypes)), + ); + + $operation = $operation->withDescription($description); + $methodWith = sprintf('with%s', $method); + $pathItem = $pathItem->$methodWith($operation); + $paths->addPath($path, $pathItem); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ShippingMethodDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ShippingMethodDocumentationModifier.php new file mode 100644 index 0000000000..3826c381ce --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ShippingMethodDocumentationModifier.php @@ -0,0 +1,80 @@ +getComponents(); + $schemas = $components->getSchemas(); + + $schemas['ShippingMethod.jsonld-shop.shipping_method.read']['properties']['price'] = [ + 'type' => 'integer', + 'readOnly' => true, + 'default' => 0, + ]; + + $this->modifyDescription($docs); + + return $docs->withComponents( + $components->withSchemas($schemas), + )->withPaths($docs->getPaths()); + } + + private function modifyDescription(OpenApi $docs): void + { + $paths = $docs->getPaths(); + + $this->addDescription($paths, sprintf('%s%s', $this->apiRoute, self::ROUTE_ADMIN_SHIPPING_METHODS), 'Post'); + $this->addDescription($paths, sprintf('%s%s', $this->apiRoute, self::ROUTE_ADMIN_SHIPPING_METHOD), 'Put'); + } + + private function addDescription(Paths $paths, string $path, string $method): void + { + $pathItem = $paths->getPath($path); + $methodGet = sprintf('get%s', $method); + $operation = $pathItem->$methodGet(); + + $description = sprintf( + "%s\n\n Allowed rule types: `%s` \n\n Allowed calculators: `%s`", + $operation->getDescription(), + implode('`, `', array_keys($this->ruleTypes)), + implode('`, `', array_keys($this->shippingMethodCalculators)), + ); + + $operation = $operation->withDescription($description); + $methodWith = sprintf('with%s', $method); + $pathItem = $pathItem->$methodWith($operation); + $paths->addPath($path, $pathItem); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ShopAuthenticationTokenDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ShopAuthenticationTokenDocumentationModifier.php new file mode 100644 index 0000000000..555481ebf8 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/ShopAuthenticationTokenDocumentationModifier.php @@ -0,0 +1,99 @@ +getComponents(); + $schemas = $components->getSchemas(); + + $schemas['ShopUserToken'] = [ + 'type' => 'object', + 'properties' => [ + 'token' => [ + 'type' => 'string', + 'readOnly' => true, + ], + 'customer' => [ + 'type' => 'string', + 'readOnly' => true, + ], + ], + ]; + + $schemas['ShopUserCredentials'] = [ + 'type' => 'object', + 'properties' => [ + 'email' => [ + 'type' => 'string', + 'example' => 'shop@example.com', + ], + 'password' => [ + 'type' => 'string', + 'example' => 'sylius', + ], + ], + ]; + + $components = $components->withSchemas($schemas); + $docs = $docs->withComponents($components); + + $docs->getPaths()->addPath( + $this->apiRoute . '/shop/authentication-token', + new PathItem( + post: new Operation( + operationId: 'postCredentialsItem', + tags: ['ShopUserToken'], + responses: [ + Response::HTTP_OK => [ + 'description' => 'Get JWT token', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/ShopUserToken', + ], + ], + ], + ], + ], + summary: 'Get JWT token to login.', + requestBody: new RequestBody( + description: 'Create new JWT Token', + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/ShopUserCredentials', + ], + ], + ]), + ), + ), + ), + ); + + return $docs; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/StatisticsDocumentationModifier.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/StatisticsDocumentationModifier.php new file mode 100644 index 0000000000..d4c6d65b87 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Documentation/StatisticsDocumentationModifier.php @@ -0,0 +1,173 @@ +> $intervalsMap */ + public function __construct( + private string $apiRoute, + private DateTimeProviderInterface $dateTimeProvider, + private array $intervalsMap, + ) { + } + + public function modify(OpenApi $docs): OpenApi + { + $schemas = $docs->getComponents()->getSchemas(); + $schemas['Statistics'] = [ + 'type' => 'object', + 'properties' => [ + 'sales' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'period' => [ + 'type' => 'string', + 'format' => 'date-time', + 'example' => '1999-12', + ], + 'total' => [ + 'type' => 'integer', + 'example' => 1000, + ], + ], + ], + ], + 'businessActivitySummary' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'totalSales' => [ + 'type' => 'integer', + 'example' => 100000, + ], + 'paidOrdersCount' => [ + 'type' => 'integer', + 'example' => 12, + ], + 'newCustomersCount' => [ + 'type' => 'integer', + 'example' => 7, + ], + 'averageOrderValue' => [ + 'type' => 'integer', + 'example' => 2500, + ], + ], + ], + ], + ], + ]; + + $path = $this->apiRoute . self::PATH; + $paths = $docs->getPaths(); + $paths->addPath($path, $this->getPathItem()); + + return $docs + ->withPaths($paths) + ->withComponents($docs->getComponents()->withSchemas($schemas)) + ; + } + + private function getPathItem(): PathItem + { + return new PathItem( + ref: 'Statistics', + summary: 'Get statistics', + get: new Operation( + operationId: 'get_statistics', + tags: ['Statistics'], + responses: [ + Response::HTTP_OK => [ + 'description' => 'Statistics', + 'content' => [ + 'application/json' => [ + 'schema' => [ + '$ref' => '#/components/schemas/Statistics', + ], + ], + ], + ], + ], + summary: 'Get statistics', + description: 'Get statistics', + parameters: $this->getParameters(), + ), + ); + } + + /** @return Parameter[] */ + private function getParameters(): array + { + $channelCode = new Parameter( + name: 'channelCode', + in: 'query', + description: 'Channel to get statistics for', + required: true, + schema: [ + 'type' => 'string', + ], + ); + + $startDate = new Parameter( + name: 'startDate', + in: 'query', + description: 'Start date for statistics', + required: true, + schema: [ + 'type' => 'string', + 'format' => 'date-time', + 'default' => $this->dateTimeProvider->now()->format('Y-01-01\T00:00:00'), + ], + ); + + $interval = new Parameter( + name: 'interval', + in: 'query', + description: 'Interval type for statistics', + required: true, + schema: [ + 'type' => 'string', + 'default' => 'month', + 'enum' => array_keys($this->intervalsMap), + ], + ); + + $endDate = new Parameter( + name: 'endDate', + in: 'query', + description: 'End date for statistics', + required: true, + schema: [ + 'type' => 'string', + 'format' => 'date-time', + 'default' => $this->dateTimeProvider->now()->format('Y-12-31\T23:59:59'), + ], + ); + + return [$channelCode, $startDate, $interval, $endDate]; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/OpenApi/Factory/OpenApiFactory.php b/src/Sylius/Bundle/ApiBundle/OpenApi/Factory/OpenApiFactory.php new file mode 100644 index 0000000000..7d5e7383a8 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/OpenApi/Factory/OpenApiFactory.php @@ -0,0 +1,44 @@ + */ + private iterable $openApiModifiers, + ) { + Assert::allIsInstanceOf($openApiModifiers, DocumentationModifierInterface::class); + } + + /** + * @param array $context + */ + public function __invoke(array $context = []): OpenApi + { + $openApi = ($this->decorated)($context); + + foreach ($this->openApiModifiers as $openApiModifier) { + $openApi = $openApiModifier->modify($openApi); + } + + return $openApi; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Payment/PaymentConfigurationProviderInterface.php b/src/Sylius/Bundle/ApiBundle/Payment/PaymentConfigurationProviderInterface.php index 32be7bf1c8..833693c7ba 100644 --- a/src/Sylius/Bundle/ApiBundle/Payment/PaymentConfigurationProviderInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Payment/PaymentConfigurationProviderInterface.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\Payment; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; -/** @experimental */ interface PaymentConfigurationProviderInterface { public function supports(PaymentMethodInterface $paymentMethod): bool; diff --git a/src/Sylius/Bundle/ApiBundle/PropertyInfo/Extractor/EmptyPropertyListExtractor.php b/src/Sylius/Bundle/ApiBundle/PropertyInfo/Extractor/EmptyPropertyListExtractor.php index 481893cf44..55c25801ba 100644 --- a/src/Sylius/Bundle/ApiBundle/PropertyInfo/Extractor/EmptyPropertyListExtractor.php +++ b/src/Sylius/Bundle/ApiBundle/PropertyInfo/Extractor/EmptyPropertyListExtractor.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\PropertyInfo\Extractor; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; -/** @experimental */ final class EmptyPropertyListExtractor implements PropertyListExtractorInterface { public function getProperties($class, array $context = []): ?array diff --git a/src/Sylius/Bundle/ApiBundle/Provider/CompositePaymentConfigurationProvider.php b/src/Sylius/Bundle/ApiBundle/Provider/CompositePaymentConfigurationProvider.php index 9ea84dc10d..6fc0ec2fd1 100644 --- a/src/Sylius/Bundle/ApiBundle/Provider/CompositePaymentConfigurationProvider.php +++ b/src/Sylius/Bundle/ApiBundle/Provider/CompositePaymentConfigurationProvider.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\Provider; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; -/** @experimental */ final class CompositePaymentConfigurationProvider implements CompositePaymentConfigurationProviderInterface { public function __construct(private iterable $apiPaymentMethodHandlers) diff --git a/src/Sylius/Bundle/ApiBundle/Provider/CompositePaymentConfigurationProviderInterface.php b/src/Sylius/Bundle/ApiBundle/Provider/CompositePaymentConfigurationProviderInterface.php index 46bd9d5ccd..335ae63148 100644 --- a/src/Sylius/Bundle/ApiBundle/Provider/CompositePaymentConfigurationProviderInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Provider/CompositePaymentConfigurationProviderInterface.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Provider; use Sylius\Component\Core\Model\PaymentInterface; -/** @experimental */ interface CompositePaymentConfigurationProviderInterface { public function provide(PaymentInterface $payment): array; diff --git a/src/Sylius/Bundle/ApiBundle/Provider/LiipProductImageFilterProvider.php b/src/Sylius/Bundle/ApiBundle/Provider/LiipProductImageFilterProvider.php index a822430c9e..38ee8801e8 100644 --- a/src/Sylius/Bundle/ApiBundle/Provider/LiipProductImageFilterProvider.php +++ b/src/Sylius/Bundle/ApiBundle/Provider/LiipProductImageFilterProvider.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Provider; -/** @experimental */ class LiipProductImageFilterProvider implements ProductImageFilterProviderInterface { private array $filters; diff --git a/src/Sylius/Bundle/ApiBundle/Provider/PathPrefixProvider.php b/src/Sylius/Bundle/ApiBundle/Provider/PathPrefixProvider.php index a61ace1be1..c047080a21 100644 --- a/src/Sylius/Bundle/ApiBundle/Provider/PathPrefixProvider.php +++ b/src/Sylius/Bundle/ApiBundle/Provider/PathPrefixProvider.php @@ -18,7 +18,6 @@ use Sylius\Component\Core\Model\AdminUserInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Symfony\Component\Security\Core\User\UserInterface; -/** @experimental */ final class PathPrefixProvider implements PathPrefixProviderInterface { public function __construct( @@ -33,6 +32,7 @@ final class PathPrefixProvider implements PathPrefixProviderInterface return null; } + /** @var array $pathElements */ $pathElements = array_values(array_filter(explode('/', str_replace($this->apiRoute, '', $path)))); if ($pathElements[0] === PathPrefixes::SHOP_PREFIX) { diff --git a/src/Sylius/Bundle/ApiBundle/Provider/PathPrefixProviderInterface.php b/src/Sylius/Bundle/ApiBundle/Provider/PathPrefixProviderInterface.php index 1166217554..fcbfb7817c 100644 --- a/src/Sylius/Bundle/ApiBundle/Provider/PathPrefixProviderInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Provider/PathPrefixProviderInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Provider; -/** @experimental */ interface PathPrefixProviderInterface { public function getPathPrefix(string $path): ?string; diff --git a/src/Sylius/Bundle/ApiBundle/Provider/ProductImageFilterProviderInterface.php b/src/Sylius/Bundle/ApiBundle/Provider/ProductImageFilterProviderInterface.php index df9a17ec9a..8713ae7a07 100644 --- a/src/Sylius/Bundle/ApiBundle/Provider/ProductImageFilterProviderInterface.php +++ b/src/Sylius/Bundle/ApiBundle/Provider/ProductImageFilterProviderInterface.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Provider; -/** @experimental */ interface ProductImageFilterProviderInterface { public function provideAllFilters(): array; diff --git a/src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntryCollection.php b/src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntryCollection.php new file mode 100644 index 0000000000..05aaa29fbc --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Query/GetAddressLogEntryCollection.php @@ -0,0 +1,27 @@ +addressId; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Query/GetCustomerStatistics.php b/src/Sylius/Bundle/ApiBundle/Query/GetCustomerStatistics.php new file mode 100644 index 0000000000..9de605bcf6 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Query/GetCustomerStatistics.php @@ -0,0 +1,27 @@ +customerId; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Query/GetStatistics.php b/src/Sylius/Bundle/ApiBundle/Query/GetStatistics.php new file mode 100644 index 0000000000..0b54d8b21a --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Query/GetStatistics.php @@ -0,0 +1,39 @@ +intervalType; + } + + public function getDatePeriod(): \DatePeriod + { + return $this->datePeriod; + } + + public function getChannelCode(): string + { + return $this->channelCode; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryCollectionHandler.php b/src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryCollectionHandler.php new file mode 100644 index 0000000000..a7abfd0ec3 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/QueryHandler/GetAddressLogEntryCollectionHandler.php @@ -0,0 +1,36 @@ + */ + public function __invoke(GetAddressLogEntryCollection $query): Collection + { + $queryBuilder = $this->addressLogEntryRepository->createByObjectIdQueryBuilder((string) $query->getAddressId()); + + return new ArrayCollection($queryBuilder->getQuery()->getResult()); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/QueryHandler/GetCustomerStatisticsHandler.php b/src/Sylius/Bundle/ApiBundle/QueryHandler/GetCustomerStatisticsHandler.php new file mode 100644 index 0000000000..9e9fb90eb7 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/QueryHandler/GetCustomerStatisticsHandler.php @@ -0,0 +1,45 @@ + $customerRepository + */ + public function __construct( + private CustomerRepositoryInterface $customerRepository, + private CustomerStatisticsProviderInterface $customerStatisticsProvider, + ) { + } + + public function __invoke(GetCustomerStatistics $query): CustomerStatistics + { + /** @var CustomerInterface|null $customer */ + $customer = $this->customerRepository->find($query->getCustomerId()); + + if ($customer === null) { + throw new CustomerNotFoundException(); + } + + return $this->customerStatisticsProvider->getCustomerStatistics($customer); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/QueryHandler/GetStatisticsHandler.php b/src/Sylius/Bundle/ApiBundle/QueryHandler/GetStatisticsHandler.php new file mode 100644 index 0000000000..e67aea9134 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/QueryHandler/GetStatisticsHandler.php @@ -0,0 +1,45 @@ + $channelRepository */ + public function __construct( + private StatisticsProviderInterface $statisticsProvider, + private ChannelRepositoryInterface $channelRepository, + ) { + } + + public function __invoke(GetStatistics $query): Statistics + { + /** @var ChannelInterface|null $channel */ + $channel = $this->channelRepository->findOneByCode($query->getChannelCode()); + + if ($channel === null) { + throw new ChannelNotFoundException( + sprintf('Channel with code "%s" does not exist.', $query->getChannelCode()), + ); + } + + return $this->statisticsProvider->provide($query->getIntervalType(), $query->getDatePeriod(), $channel); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AccountResetPassword.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AccountResetPassword.xml index f050313635..5c3c95a47f 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AccountResetPassword.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AccountResetPassword.xml @@ -20,14 +20,17 @@ input - + POST /shop/reset-password-requests Sylius\Bundle\ApiBundle\Command\Account\RequestResetPasswordToken false 202 - shop:reset_password:create + + shop:reset_password:create + sylius:shop:reset_password:create + Requests password reset @@ -36,14 +39,17 @@ - + PATCH /shop/reset-password-requests/{resetPasswordToken} Sylius\Bundle\ApiBundle\Command\Account\ResetPassword false 202 - shop:reset_password:update + + shop:reset_password:update + sylius:shop:reset_password:update + Resets password diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml index 5fcbec7d3c..ef08ee59f7 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Address.xml @@ -19,15 +19,27 @@ GET /shop/addresses - shop:address:read + + shop:address:index + sylius:shop:address:index + POST /shop/addresses + + + shop:address:create + sylius:shop:address:create + + - shop:address:create + + shop:address:show + sylius:shop:address:show + @@ -38,16 +50,53 @@ /admin/addresses/{id} - admin:address:read + admin:address:show + sylius:admin:address:show + + PUT + /admin/addresses/{id} + + + admin:address:update + sylius:admin:address:update + + + + + admin:address:show + sylius:admin:address:show + + + + + + GET + /admin/addresses/{id}/log-entries + Sylius\Bundle\ApiBundle\Controller\GetAddressLogEntryCollectionAction + + + admin:address:log_entry:show + sylius:admin:address:log_entry:show + + + + Retrieves the collection of AddressLogEntry resources. + Retrieves the collection of AddressLogEntry resources. + + + GET /shop/addresses/{id} - shop:address:read + + shop:address:show + sylius:shop:address:show + @@ -60,7 +109,16 @@ PUT /shop/addresses/{id} - shop:address:update + + shop:address:update + sylius:shop:address:update + + + + + shop:address:show + sylius:shop:address:show + @@ -72,6 +130,7 @@ + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Adjustment.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Adjustment.xml index 1cc5c7d297..45d2466a93 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Adjustment.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Adjustment.xml @@ -23,7 +23,10 @@ GET /admin/adjustments/{id} - admin:adjustment:read + + admin:adjustment:show + sylius:admin:adjustment:show + @@ -31,7 +34,10 @@ GET /shop/adjustments/{id} - shop:adjustment:read + + shop:adjustment:show + sylius:shop:adjustment:show + @@ -39,12 +45,46 @@ - shop:cart:read + + shop:cart:show + sylius:shop:cart:show + + - shop:cart:read + + shop:cart:show + sylius:shop:cart:show + + + + + + + + admin:order_item:index + sylius:admin:order_item:index + + + + + + + + admin:order_item_unit:index + sylius:admin:order_item_unit:index + + + + + + + + admin:shipment:index + sylius:admin:shipment:index + @@ -54,8 +94,9 @@ - - + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AdminResetPassword.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AdminResetPassword.xml index 197eef1dfc..86aadf5413 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AdminResetPassword.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AdminResetPassword.xml @@ -27,7 +27,10 @@ false 202 - admin:reset_password:create + + admin:reset_password:create + sylius:admin:reset_password:create + Requests administrator's password reset @@ -43,7 +46,10 @@ false 202 - admin:reset_password:update + + admin:reset_password:update + sylius:admin:reset_password:update + Resets administrator's password diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AdminUser.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AdminUser.xml index 3d9a71e4c0..817a4551c3 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AdminUser.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AdminUser.xml @@ -24,7 +24,10 @@ GET - admin:admin_user:read + + admin:admin_user:index + sylius:admin:admin_user:index + @@ -34,8 +37,17 @@ sylius sylius_user_create + + + admin:admin_user:create + sylius:admin:admin_user:create + + - admin:admin_user:create + + admin:admin_user:show + sylius:admin:admin_user:show + @@ -44,14 +56,26 @@ GET - admin:admin_user:read + + admin:admin_user:show + sylius:admin:admin_user:show + PUT - admin:admin_user:update + + admin:admin_user:update + sylius:admin:admin_user:update + + + + + admin:admin_user:show + sylius:admin:admin_user:show + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AvatarImage.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AvatarImage.xml index a60aba7dac..20e7f9fcc1 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AvatarImage.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/AvatarImage.xml @@ -45,7 +45,10 @@ - admin:avatar_image:read + + admin:avatar_image:show + sylius:admin:avatar_image:show + @@ -54,7 +57,10 @@ GET - admin:avatar_image:read + + admin:avatar_image:show + sylius:admin:avatar_image:show + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CatalogPromotion.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CatalogPromotion.xml index 8f5caa6927..cbb18e6baa 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CatalogPromotion.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CatalogPromotion.xml @@ -28,21 +28,35 @@ sylius.api.catalog_promotion_enabled_filter sylius.api.catalog_promotion_start_date_filter sylius.api.catalog_promotion_end_date_filter - Sylius\Bundle\ApiBundle\Filter\Doctrine\CatalogPromotionChannelFilter + Sylius\Bundle\ApiBundle\Filter\Doctrine\ChannelsAwareChannelFilter + sylius.api.order_filter.code + sylius.api.order_filter.name + sylius.api.order_filter.start_date + sylius.api.order_filter.end_date + sylius.api.order_filter.priority - admin:catalog_promotion:read + + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + /admin/catalog-promotions POST - - admin:catalog_promotion:read - - admin:catalog_promotion:create + + admin:catalog_promotion:create + sylius:admin:catalog_promotion:create + + + + + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show + @@ -79,7 +93,10 @@ Example configuration for `percentage_discount` action type: /admin/catalog-promotions/{code} GET - admin:catalog_promotion:read + + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show + @@ -87,18 +104,27 @@ Example configuration for `percentage_discount` action type: /shop/catalog-promotions/{code} GET - shop:catalog_promotion:read + + shop:catalog_promotion:show + sylius:shop:catalog_promotion:show + /admin/catalog-promotions/{code} PUT - - admin:catalog_promotion:read - - admin:catalog_promotion:update + + admin:catalog_promotion:update + sylius:admin:catalog_promotion:update + + + + + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show + @@ -164,7 +190,6 @@ Example configuration for `percentage_discount` action type: string string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CatalogPromotionTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CatalogPromotionTranslation.xml index 28ed174fec..bebbdac5e7 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CatalogPromotionTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CatalogPromotionTranslation.xml @@ -24,6 +24,12 @@ GET /admin/catalog-promotion-translations/{id} + + + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Channel.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Channel.xml index e041122a9f..213fd2c421 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Channel.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Channel.xml @@ -21,7 +21,10 @@ GET /admin/channels - admin:channel:read + + admin:channel:index + sylius:admin:channel:index + @@ -29,7 +32,10 @@ GET /shop/channels - shop:channel:read + + shop:channel:index + sylius:shop:channel:index + @@ -37,8 +43,18 @@ POST /admin/channels - admin:channel:create + + admin:channel:create + sylius:admin:channel:create + + + + admin:channel:show + sylius:admin:channel:show + + + sylius @@ -50,7 +66,10 @@ Use $code to retrieve a channel resource. - admin:channel:read + + admin:channel:show + sylius:admin:channel:show + @@ -58,7 +77,10 @@ GET /shop/channels/{code} - shop:channel:read + + shop:channel:show + sylius:shop:channel:show + Use $code to retrieve a channel resource. @@ -69,8 +91,23 @@ PUT /admin/channels/{code} - admin:channel:update + + admin:channel:update + sylius:admin:channel:update + + + + admin:channel:show + sylius:admin:channel:show + + + sylius + + + + DELETE + /admin/channels/{code} @@ -84,6 +121,7 @@ + @@ -105,5 +143,6 @@ + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ChannelPriceHistoryConfig.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ChannelPriceHistoryConfig.xml new file mode 100644 index 0000000000..c6aea8c0b5 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ChannelPriceHistoryConfig.xml @@ -0,0 +1,56 @@ + + + + + + + admin + sylius + + + + + + GET + + + admin:channel_price_history_config:show + sylius:admin:channel_price_history_config:show + + + + + PUT + + + admin:channel_price_history_config:update + sylius:admin:channel_price_history_config:update + + + + + admin:channel_price_history_config:show + sylius:admin:channel_price_history_config:show + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ChannelPricingLogEntry.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ChannelPricingLogEntry.xml new file mode 100644 index 0000000000..d4898533a1 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ChannelPricingLogEntry.xml @@ -0,0 +1,58 @@ + + + + + + + + + GET + /admin/channel-pricing-log-entries + + + admin:channel_pricing_log_entry:index + sylius:admin:channel_pricing_log_entry:index + + + + sylius.api.channel_pricing_channel_filter + sylius.api.channel_pricing_product_variant_filter + + + DESC + + + + + + + GET + /admin/channel-pricing-log-entries/{id} + + + admin:channel_pricing_log_entry:show + sylius:admin:channel_pricing_log_entry:show + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ContactRequest.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ContactRequest.xml index 9e4feba9ce..c04f66645a 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ContactRequest.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ContactRequest.xml @@ -15,14 +15,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd" > - + shop input sylius false - - POST @@ -30,14 +28,19 @@ Sylius\Bundle\ApiBundle\Command\SendContactRequest 202 - shop:contact_request:create + + shop:contact_request:create + sylius:shop:contact_request:create + - Send contact request + Sends contact request + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Country.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Country.xml index f66d461bc7..b1657c2bcd 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Country.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Country.xml @@ -23,7 +23,10 @@ GET /admin/countries - admin:country:read + + admin:country:index + sylius:admin:country:index + @@ -31,7 +34,10 @@ GET /shop/countries - shop:country:read + + shop:country:index + sylius:shop:country:index + @@ -39,7 +45,16 @@ POST /admin/countries - admin:country:create + + admin:country:create + sylius:admin:country:create + + + + + admin:country:show + sylius:admin:country:show + @@ -49,14 +64,20 @@ GET /admin/countries/{code} - admin:country:read + + admin:country:show + sylius:admin:country:show + GET /shop/countries/{code} - shop:country:read + + shop:country:show + sylius:shop:country:show + @@ -64,7 +85,16 @@ PUT /admin/countries/{code} - admin:country:update + + admin:country:update + sylius:admin:country:update + + + + + admin:country:show + sylius:admin:country:show + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Currency.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Currency.xml index f54e6a1434..5000f4059c 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Currency.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Currency.xml @@ -23,13 +23,28 @@ GET /admin/currencies - admin:currency:read + + admin:currency:index + sylius:admin:currency:index + POST /admin/currencies + + + admin:currency:create + sylius:admin:currency:create + + + + + admin:currency:show + sylius:admin:currency:show + + @@ -37,7 +52,10 @@ shop /shop/currencies - shop:currency:read + + shop:currency:index + sylius:shop:currency:index + @@ -47,7 +65,10 @@ GET /admin/currencies/{code} - admin:currency:read + + admin:currency:show + sylius:admin:currency:show + @@ -56,7 +77,10 @@ shop /shop/currencies/{code} - shop:currency:read + + shop:currency:show + sylius:shop:currency:show + @@ -65,5 +89,6 @@ + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Customer.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Customer.xml index 9fe9ea41d0..e9f47849ed 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Customer.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Customer.xml @@ -16,10 +16,45 @@ xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd" > - sylius + + GET + /admin/customers + + + admin:customer:index + sylius:admin:customer:index + + + + sylius.api.customer_group_filter + + + + + POST + /admin/customers + + + admin:customer:create + sylius:admin:customer:create + + + + + admin:customer:show + sylius:admin:customer:show + + + + sylius + sylius_user_create + sylius_api_user_create + + + POST /shop/customers @@ -27,7 +62,10 @@ Registers a new customer - shop:customer:create + + shop:customer:create + sylius:shop:customer:create + input Sylius\Bundle\ApiBundle\Command\Account\RegisterShopUser @@ -40,16 +78,59 @@ GET /admin/customers/{id} - admin:customer:read + + admin:customer:show + sylius:admin:customer:show + + + GET + /admin/customers/{id}/statistics + Sylius\Bundle\ApiBundle\Controller\GetCustomerStatisticsAction + + + admin:customer:statistics:show + sylius:admin:customer:statistics:show + + + + + + PUT + /admin/customers/{id} + + + admin:customer:update + sylius:admin:customer:update + + + + + admin:customer:show + sylius:admin:customer:show + + + + sylius + sylius_api_user_create + + + + + DELETE + /admin/customers/{id}/user + Sylius\Bundle\ApiBundle\Controller\RemoveCustomerShopUserAction + + GET /shop/customers/{id} - shop:customer:read + shop:customer:show + sylius:shop:customer:show @@ -61,7 +142,10 @@ Sylius\Bundle\ApiBundle\Command\Account\ChangeShopUserPassword false - shop:customer:password:update + + shop:customer:password:update + sylius:shop:customer:password:update + Change password for logged in customer @@ -72,10 +156,16 @@ PUT /shop/customers/{id} - shop:customer:update + + shop:customer:update + sylius:shop:customer:update + - shop:customer:read + + shop:customer:show + sylius:shop:customer:show + @@ -87,6 +177,6 @@ - + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CustomerGroup.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CustomerGroup.xml index 4fd76513ca..26a9ff11c5 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CustomerGroup.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/CustomerGroup.xml @@ -23,14 +23,26 @@ GET - admin:customer_group:read + + admin:customer_group:index + sylius:admin:customer_group:index + POST - admin:customer_group:create + + admin:customer_group:create + sylius:admin:customer_group:create + + + + + admin:customer_group:show + sylius:admin:customer_group:show + @@ -39,14 +51,26 @@ GET - admin:customer_group:read + + admin:customer_group:show + sylius:admin:customer_group:show + PUT - admin:customer_group:update + + admin:customer_group:update + sylius:admin:customer_group:update + + + + + admin:customer_group:show + sylius:admin:customer_group:show + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ExchangeRate.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ExchangeRate.xml index 9546d7be3d..56dcf31dd0 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ExchangeRate.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ExchangeRate.xml @@ -26,7 +26,10 @@ Sylius\Bundle\ApiBundle\Filter\Doctrine\ExchangeRateFilter - admin:exchange_rate:read + + admin:exchange_rate:index + sylius:admin:exchange_rate:index + @@ -34,7 +37,10 @@ GET /shop/exchange-rates - shop:exchange_rate:read + + shop:exchange_rate:index + sylius:shop:exchange_rate:index + @@ -42,7 +48,16 @@ POST /admin/exchange-rates - admin:exchange_rate:create + + admin:exchange_rate:create + sylius:admin:exchange_rate:create + + + + + admin:exchange_rate:show + sylius:admin:exchange_rate:show + @@ -52,7 +67,10 @@ GET /admin/exchange-rates/{id} - admin:exchange_rate:read + + admin:exchange_rate:show + sylius:admin:exchange_rate:show + @@ -60,7 +78,10 @@ GET /shop/exchange-rates/{id} - shop:exchange_rate:read + + shop:exchange_rate:show + sylius:shop:exchange_rate:show + @@ -68,7 +89,16 @@ PUT /admin/exchange-rates/{id} - admin:exchange_rate:update + + admin:exchange_rate:update + sylius:admin:exchange_rate:update + + + + + admin:exchange_rate:show + sylius:admin:exchange_rate:show + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/GatewayConfig.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/GatewayConfig.xml new file mode 100644 index 0000000000..e7f7f014c7 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/GatewayConfig.xml @@ -0,0 +1,55 @@ + + + + + + + admin + + + sylius + + + + + + + GET + + + admin:gateway_config:show + sylius:admin:gateway_config:show + + + + + + + + + + admin:gateway_config:show + sylius:admin:gateway_config:show + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Locale.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Locale.xml index a69477b913..f12de7cf12 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Locale.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Locale.xml @@ -24,16 +24,27 @@ GET /admin/locales - admin:locale:read + + admin:locale:index + sylius:admin:locale:index + - POST /admin/locales - admin:locale:create + + admin:locale:create + sylius:admin:locale:create + + + + + admin:locale:show + sylius:admin:locale:show + @@ -41,7 +52,10 @@ GET /shop/locales - shop:locale:read + + shop:locale:index + sylius:shop:locale:index + @@ -51,15 +65,26 @@ GET /admin/locales/{code} - admin:locale:read + + admin:locale:show + sylius:admin:locale:show + + + DELETE + admin/locales/{code} + + GET /shop/locales/{code} - shop:locale:read + + shop:locale:show + sylius:shop:locale:show + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Order.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Order.xml index 17592fbf9d..5f4f36a041 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Order.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Order.xml @@ -22,8 +22,25 @@ GET /admin/orders + + sylius.api.order_channel_filter + sylius.api.order_currency_filter + sylius.api.order_customer_filter + sylius.api.order_date_filter + sylius.api.order_number_filter + sylius.api.order_product_filter + sylius.api.order_shipping_method_filter + sylius.api.order_total_filter + sylius.api.order_variants_filter + + + DESC + - admin:order:read + + admin:order:index + sylius:admin:order:index + @@ -32,14 +49,19 @@ /shop/orders input Sylius\Bundle\ApiBundle\Command\Cart\PickupCart - + - shop:order:read - shop:cart:read + shop:order:create + sylius:shop:order:create - - shop:order:create + + + shop:order:show + sylius:shop:order:show + shop:cart:show + sylius:shop:cart:show + Pickups a new cart @@ -51,7 +73,8 @@ /shop/orders - shop:order:read + shop:order:index + sylius:shop:order:index @@ -62,7 +85,23 @@ GET /admin/orders/{tokenValue} - admin:order:read + + admin:order:show + sylius:admin:order:show + + + + + + GET + /admin/orders/{tokenValue}/adjustments + Sylius\Bundle\ApiBundle\Controller\GetOrderAdjustmentsAction + false + + + admin:adjustment:show + sylius:admin:adjustment:show + @@ -70,7 +109,10 @@ GET /shop/orders/{tokenValue} - shop:cart:read + + shop:cart:show + sylius:shop:cart:show + @@ -81,7 +123,10 @@ Deletes cart - shop:order:read + + shop:order:show + sylius:shop:order:show + @@ -90,13 +135,17 @@ /admin/orders/{tokenValue}/cancel false Sylius\Bundle\ApiBundle\Applicator\OrderStateMachineTransitionApplicatorInterface::cancel - + - admin:order:read + admin:order:update + sylius:admin:order:update - - admin:order:update + + + admin:order:show + sylius:admin:order:show + Cancels Order @@ -110,11 +159,15 @@ Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart - shop:cart:read + shop:cart:show + sylius:shop:cart:show - shop:cart:add_item + + shop:cart:add_item + sylius:shop:cart:add_item + Adds Item to cart @@ -130,12 +183,17 @@ input Sylius\Bundle\ApiBundle\Command\Checkout\ChooseShippingMethod - shop:cart:select_shipping_method + + shop:cart:select_shipping_method + sylius:shop:cart:select_shipping_method + - shop:order:read - shop:cart:read + shop:order:show + sylius:shop:order:show + shop:cart:show + sylius:shop:cart:show @@ -167,12 +225,17 @@ input Sylius\Bundle\ApiBundle\Command\Checkout\ChoosePaymentMethod - shop:cart:select_payment_method + + shop:cart:select_payment_method + sylius:shop:cart:select_payment_method + - shop:order:read - shop:cart:read + shop:order:show + sylius:shop:order:show + shop:cart:show + sylius:shop:cart:show @@ -204,10 +267,16 @@ input Sylius\Bundle\ApiBundle\Command\Account\ChangePaymentMethod - shop:order:account:change_payment_method + + shop:order:account:change_payment_method + sylius:shop:order:account:change_payment_method + - shop:order:account:read + + shop:order:account:show + sylius:shop:order:account:show + Change the payment method as logged shop user @@ -269,12 +338,17 @@ input Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder - shop:cart:complete + + shop:cart:complete + sylius:shop:cart:complete + - shop:order:read - shop:cart:read + shop:order:show + sylius:shop:order:show + shop:cart:show + sylius:shop:cart:show @@ -288,7 +362,10 @@ Sylius\Bundle\ApiBundle\Controller\DeleteOrderItemAction false - shop:cart:remove_item + + shop:cart:remove_item + sylius:shop:cart:remove_item + @@ -317,11 +394,17 @@ /shop/orders/{tokenValue}/items/{orderItemId} input Sylius\Bundle\ApiBundle\Command\Cart\ChangeItemQuantityInCart - - shop:cart:read - - shop:cart:change_quantity + + shop:cart:change_quantity + sylius:shop:cart:change_quantity + + + + + shop:cart:show + sylius:shop:cart:show + Changes quantity of order item @@ -352,18 +435,35 @@ input Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart - shop:cart:update + + shop:cart:update + sylius:shop:cart:update + - shop:order:read - shop:cart:read + shop:order:show + sylius:shop:order:show + shop:cart:show + sylius:shop:cart:show Addresses cart to given location, logged in Customer does not have to provide an email. Applies coupon to cart. + + + POST + /admin/orders/{tokenValue}/resend-confirmation-email + input + Sylius\Bundle\ApiBundle\Command\ResendOrderConfirmationEmail + false + 202 + + Resends order confirmation email + + @@ -409,12 +509,14 @@ + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/OrderItem.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/OrderItem.xml index 24d96b9169..b1387c656b 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/OrderItem.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/OrderItem.xml @@ -23,7 +23,10 @@ GET /admin/order-items/{id} - admin:order_item:read + + admin:order_item:show + sylius:admin:order_item:show + @@ -31,8 +34,10 @@ GET /shop/order-items/{id} - shop:order_item:read - shop:cart:read + + shop:order_item:show + sylius:shop:order_item:show + @@ -41,13 +46,14 @@ GET /admin/order-items/{id}/adjustments - - admin:order_item:read - + - shop:cart:read + + shop:cart:show + sylius:shop:cart:show + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/OrderItemUnit.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/OrderItemUnit.xml index a17b7d766a..3b5244e553 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/OrderItemUnit.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/OrderItemUnit.xml @@ -23,7 +23,10 @@ GET /admin/order-item-units/{id} - admin:order_item_unit:read + + admin:order_item_unit:show + sylius:admin:order_item_unit:show + @@ -31,12 +34,24 @@ GET /shop/order-item-units/{id} - shop:order_item_unit:read + + shop:order_item_unit:show + sylius:shop:order_item_unit:show + + + + GET + /admin/order-item-units/{id}/adjustments + + + - + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Payment.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Payment.xml index 5784f06b57..195a663296 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Payment.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Payment.xml @@ -30,7 +30,10 @@ sylius.api.search_payment_filter - admin:payment:read + + admin:payment:index + sylius:admin:payment:index + @@ -40,7 +43,10 @@ GET /admin/payments/{id} - admin:payment:read + + admin:payment:show + sylius:admin:payment:show + @@ -48,7 +54,10 @@ GET /shop/payments/{id} - shop:payment:read + + shop:payment:show + sylius:shop:payment:show + @@ -63,6 +72,17 @@ + + + + + admin:payment:show + sylius:admin:payment:show + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PaymentMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PaymentMethod.xml index e9798b4e6f..9147764f8f 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PaymentMethod.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PaymentMethod.xml @@ -16,6 +16,12 @@ xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd" > + Sylius\Bundle\PayumBundle\Validator\GroupsGenerator\GatewayConfigGroupsGenerator + + + ASC + + GET @@ -26,7 +32,40 @@ - shop:payment_method:read + shop:payment_method:index + sylius:shop:payment_method:index + + + + + + GET + /admin/payment-methods + + sylius.api.payment_method_order_filter + Sylius\Bundle\ApiBundle\Filter\Doctrine\TranslationOrderNameAndLocaleFilter + + + + admin:payment_method:index + sylius:admin:payment_method:index + + + + + + POST + /admin/payment-methods + + + admin:payment_method:create + sylius:admin:payment_method:create + + + + + admin:payment_method:show + sylius:admin:payment_method:show @@ -37,21 +76,73 @@ GET /admin/payment-methods/{code} - admin:payment_method:read + + admin:payment_method:show + sylius:admin:payment_method:show + + + PUT + /admin/payment-methods/{code} + + + admin:payment_method:update + sylius:admin:payment_method:update + + + + + admin:payment_method:show + sylius:admin:payment_method:show + + + + + + DELETE + /admin/payment-methods/{code} + + GET /shop/payment-methods/{code} - shop:payment_method:read + + shop:payment_method:show + sylius:shop:payment_method:show + + + + GET + /admin/payment-methods/{code}/gateway-config + + + - + + + + + + + + + object + + + string + string + string + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PaymentMethodTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PaymentMethodTranslation.xml new file mode 100644 index 0000000000..2f19e2b625 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PaymentMethodTranslation.xml @@ -0,0 +1,36 @@ + + + + + + + sylius + + + + + + GET + /admin/payment-method-translations/{id} + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Product.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Product.xml index 6b893c3fd9..e64958ef93 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Product.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Product.xml @@ -30,10 +30,14 @@ sylius.api.product_name_filter sylius.api.product_order_filter sylius.api.product_taxon_code_filter + Sylius\Bundle\ApiBundle\Filter\Doctrine\ChannelsAwareChannelFilter Sylius\Bundle\ApiBundle\Filter\Doctrine\TranslationOrderNameAndLocaleFilter - admin:product:read + + admin:product:index + sylius:admin:product:index + @@ -49,7 +53,10 @@ Sylius\Bundle\ApiBundle\Filter\Doctrine\TaxonFilter - shop:product:read + + shop:product:index + sylius:shop:product:index + @@ -57,7 +64,16 @@ POST /admin/products - admin:product:create + + admin:product:create + sylius:admin:product:create + + + + + admin:product:show + sylius:admin:product:show + @@ -70,7 +86,10 @@ Use code to retrieve a product resource. - admin:product:read + + admin:product:show + sylius:admin:product:show + @@ -81,7 +100,10 @@ Use code to retrieve a product resource. - shop:product:read + + shop:product:show + sylius:shop:product:show + @@ -104,7 +126,10 @@ - shop:product:read + + shop:product:show + sylius:shop:product:show + @@ -112,7 +137,16 @@ PUT /admin/products/{code} - admin:product:update + + admin:product:update + sylius:admin:product:update + + + + + admin:product:show + sylius:admin:product:show + @@ -123,12 +157,14 @@ + + GET + /admin/products/{code}/images + + GET /shop/products/{code}/attributes - - shop:product:read - @@ -143,7 +179,10 @@ string string - string + string + string + string + string @@ -164,5 +203,8 @@ + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociation.xml index 54b1f21de5..da23ad4b85 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociation.xml @@ -18,15 +18,80 @@ sylius - + + + GET + /admin/product-associations + + sylius.api.product_association_filter + + + + admin:product_association:index + sylius:admin:product_association:index + + + + + + POST + /admin/product-associations + + + admin:product_association:create + sylius:admin:product_association:create + + + + + admin:product_association:show + sylius:admin:product_association:show + + + + + + GET + /admin/product-associations/{id} + + + admin:product_association:show + sylius:admin:product_association:show + + + + + + PUT + /admin/product-associations/{id} + + + admin:product_association:update + sylius:admin:product_association:update + + + + + admin:product_association:show + sylius:admin:product_association:show + + + + + + DELETE + /admin/product-associations/{id} + + GET /shop/product-associations/{id} - shop:product_association:read + shop:product_association:show + sylius:shop:product_association:show @@ -34,6 +99,7 @@ + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociationType.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociationType.xml index 5bedf7cd0a..b14635dfbf 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociationType.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAssociationType.xml @@ -26,21 +26,27 @@ sylius.api.product_association_type_filter - admin:product_association_type:read + + admin:product_association_type:index + sylius:admin:product_association_type:index + POST /admin/product-association-types - - admin:product_association_type:read - - admin:product_association_type:create + + admin:product_association_type:create + sylius:admin:product_association_type:create + - admin:product_association_type:read + + admin:product_association_type:show + sylius:admin:product_association_type:show + @@ -50,7 +56,10 @@ GET /admin/product-association-types/{code} - admin:product_association_type:read + + admin:product_association_type:show + sylius:admin:product_association_type:show + @@ -58,18 +67,27 @@ GET /shop/product-association-types/{code} - shop:product_association_type:read + + shop:product_association_type:show + sylius:shop:product_association_type:show + PUT /admin/product-association-types/{code} - - admin:product_association_type:read - - admin:product_association_type:update + + admin:product_association_type:update + sylius:admin:product_association_type:update + + + + + admin:product_association_type:show + sylius:admin:product_association_type:show + @@ -89,8 +107,6 @@ string - string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttribute.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttribute.xml index 4986940391..38cb074e7e 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttribute.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttribute.xml @@ -16,20 +16,208 @@ xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd" > - - - shop:product_attribute:read - - - sylius - + + ASC + + + + + GET + /admin/product-attributes + + sylius.api.order_filter.position + + + + admin:product_attribute:index + sylius:admin:product_attribute:index + + + + + + POST + /admin/product-attributes + + + admin:product_attribute:create + sylius:admin:product_attribute:create + + + + + admin:product_attribute:show + sylius:admin:product_attribute:show + + + + +Example configuration for a `text` type attribute: + +``` +{ + "type": "text", + "configuration": { + "min": 5, + "max": 255 + } +} +``` + +Example configuration for a `datetime` type attribute: + +``` +{ + "type": "datetime", + "configuration": { + "format": "Y-m-d H:i:s" + } +} +``` + +Example configuration for a `date` type attribute: + +``` +{ + "type": "date", + "configuration": { + "format": "Y-m-d" + } +} +``` + +Example configuration for a `select` type attribute: + +``` +{ + "type": "select", + "configuration": { + "choices": { + "0afb212e-cd08-11ec-871e-0242ac120005": { + "en_US": "Plastic", + "fr_FR": "Plastique" + }, + "3bfb211f-cd08-11ec-871e-0242ac120005": { + "en_US": "Cotton", + "fr_FR": "Coton" + } + }, + "multiple": true, + "min": 1, + "max": 3 + } +} +``` + + + + + + GET + /admin/product-attributes/{code} + + + admin:product_attribute:show + sylius:admin:product_attribute:show + + + + + + DELETE + /admin/product-attributes/{code} + + + + PUT + /admin/product-attributes/{code} + + + admin:product_attribute:update + sylius:admin:product_attribute:update + + + + + admin:product_attribute:show + sylius:admin:product_attribute:show + + + + +Example configuration for a `text` type attribute: + +``` +{ + "type": "text", + "configuration": { + "min": 5, + "max": 255 + } +} +``` + +Example configuration for a `datetime` type attribute: + +``` +{ + "type": "datetime", + "configuration": { + "format": "Y-m-d H:i:s" + } +} +``` + +Example configuration for a `date` type attribute: + +``` +{ + "type": "date", + "configuration": { + "format": "Y-m-d" + } +} +``` + +Example configuration for a `select` type attribute: + +``` +{ + "type": "select", + "configuration": { + "choices": { + "0afb212e-cd08-11ec-871e-0242ac120005": { + "en_US": "Plastic", + "fr_FR": "Plastique" + }, + "3bfb211f-cd08-11ec-871e-0242ac120005": { + "en_US": "Cotton", + "fr_FR": "Coton" + } + }, + "multiple": true, + "min": 1, + "max": 3 + } +} +``` + + + + GET /shop/product-attributes/{code} + + + shop:product_attribute:show + sylius:shop:product_attribute:show + + @@ -39,6 +227,17 @@ + + + + object + + + string + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttributeTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttributeTranslation.xml new file mode 100644 index 0000000000..2eff9520ec --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttributeTranslation.xml @@ -0,0 +1,42 @@ + + + + + + + sylius + + + + + + GET + /admin/product-attribute-translations/{id} + + + admin:product_attribute:show + sylius:admin:product_attribute:show + admin:product_attribute_translation:show + sylius:admin:product_attribute_translation:show + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttributeValue.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttributeValue.xml index 773971936e..41265b1527 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttributeValue.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductAttributeValue.xml @@ -16,12 +16,6 @@ xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd" > - - - shop:product_attribute_value:read - - - sylius @@ -31,12 +25,40 @@ + + GET + /admin/product-attribute-values/{id} + + + admin:product_attribute_value:show + sylius:admin:product_attribute_value:show + + + + GET /shop/product-attribute-values/{id} + + + shop:product_attribute_value:show + sylius:shop:product_attribute_value:show + + + + + + + shop:product_attribute_value:show + sylius:shop:product_attribute_value:show + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductImage.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductImage.xml index 5c983309f9..6e40a729b8 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductImage.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductImage.xml @@ -11,8 +11,70 @@ GET /admin/product-images + + sylius.api.product_image_product_variants_filter + - admin:product_image:read + + admin:product_image:index + sylius:admin:product_image:index + + + + + + POST + /admin/products/{code}/images + Sylius\Bundle\ApiBundle\Controller\UploadProductImageAction + false + + + + code + path + true + + string + + + + + + + + object + + + string + binary + + + string + + + array + + string + iri-reference + + + + + + + form + true + + + + + + + + + admin:product_image:show + sylius:admin:product_image:show + @@ -22,14 +84,21 @@ GET /admin/product-images/{id} - admin:product_image:read + + admin:product_image:show + sylius:admin:product_image:show + + GET /shop/product-images/{id} - shop:product_image:read + + shop:product_image:show + sylius:shop:product_image:show + @@ -44,10 +113,44 @@ + + + PUT + /admin/product-images/{id} + + + admin:product_image:update + sylius:admin:product_image:update + + + + + admin:product_image:show + sylius:admin:product_image:show + + + + + + DELETE + /admin/product-images/{id} + + + + + + admin:product_image:show + sylius:admin:product_image:show + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductOption.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductOption.xml index 2f96188567..5480089561 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductOption.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductOption.xml @@ -27,18 +27,27 @@ GET /admin/product-options - admin:product_option:read + + admin:product_option:index + sylius:admin:product_option:index + POST /admin/product-options - - admin:product_option:read - - admin:product_option:create + + admin:product_option:create + sylius:admin:product_option:create + + + + + admin:product_option:show + sylius:admin:product_option:show + @@ -48,7 +57,10 @@ GET /admin/product-options/{code} - admin:product_option:read + + admin:product_option:show + sylius:admin:product_option:show + @@ -56,19 +68,33 @@ GET /shop/product-options/{code} - shop:product_option:read + + shop:product_option:show + sylius:shop:product_option:show + PUT /admin/product-options/{code} - - admin:product_option:read - - admin:product_option:update + + admin:product_option:update + sylius:admin:product_option:update + + + + admin:product_option:show + sylius:admin:product_option:show + + + + + + DELETE + /admin/product-options/{code} @@ -76,9 +102,6 @@ GET /admin/product-options/{code}/values - - admin:product_option:read - @@ -95,7 +118,6 @@ string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductOptionValue.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductOptionValue.xml index 855b35e180..2a3841cb54 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductOptionValue.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductOptionValue.xml @@ -25,7 +25,10 @@ GET /admin/product-option-values/{code} - admin:product_option_value:read + + admin:product_option_value:show + sylius:admin:product_option_value:show + @@ -33,11 +36,25 @@ GET /shop/product-option-values/{code} - shop:product_option_value:read + + shop:product_option_value:show + sylius:shop:product_option_value:show + + + + + + admin:product_option_value:show + sylius:admin:product_option_value:show + + + + + @@ -46,7 +63,6 @@ string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductReview.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductReview.xml index 74662382df..eb00645925 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductReview.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductReview.xml @@ -23,8 +23,14 @@ GET /admin/product-reviews + + sylius.api.product_review_status_filter + - admin:product_review:read + + admin:product_review:index + sylius:admin:product_review:index + @@ -34,7 +40,16 @@ input Sylius\Bundle\ApiBundle\Command\Catalog\AddProductReview - shop:product_review:create + + shop:product_review:create + sylius:shop:product_review:create + + + + + shop:product_review:show + sylius:shop:product_review:show + @@ -46,7 +61,10 @@ sylius.api.product_review_date_filter - shop:product_review:read + + shop:product_review:index + sylius:shop:product_review:index + @@ -56,7 +74,10 @@ GET /admin/product-reviews/{id} - admin:product_review:read + + admin:product_review:show + sylius:admin:product_review:show + @@ -64,7 +85,10 @@ GET /shop/product-reviews/{id} - shop:product_review:read + + shop:product_review:show + sylius:shop:product_review:show + @@ -72,7 +96,10 @@ DELETE /admin/product-reviews/{id} - admin:product_review:update + + admin:product_review:update + sylius:admin:product_review:update + @@ -80,10 +107,16 @@ PUT /admin/product-reviews/{id} - product_review:update + + admin:product_review:update + sylius:admin:product_review:update + - - admin:product_review:update + + + admin:product_review:show + sylius:admin:product_review:show + @@ -95,8 +128,11 @@ Accepts Product Review - - admin:product_review:update + + + admin:product_review:show + sylius:admin:product_review:show + @@ -108,8 +144,11 @@ Rejects Product Review - - admin:product_review:update + + + admin:product_review:show + sylius:admin:product_review:show + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTaxon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTaxon.xml index e9779bbd7d..12c0ef663f 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTaxon.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTaxon.xml @@ -1,5 +1,16 @@ + + sylius + + ASC + + GET /admin/product-taxons + + sylius.api.search_filter.product.code + sylius.api.search_filter.taxon.code + sylius.api.order_filter.position + - admin:product_taxon:read + + admin:product_taxon:index + sylius:admin:product_taxon:index + + + + + + POST + /admin/product-taxons + + + admin:product_taxon:create + sylius:admin:product_taxon:create + + + + + admin:product_taxon:show + sylius:admin:product_taxon:show + @@ -22,16 +62,45 @@ GET /admin/product-taxons/{id} - admin:product_taxon:read + + admin:product_taxon:show + sylius:admin:product_taxon:show + + + + PUT + /admin/product-taxons/{id} + + + admin:product_taxon:update + sylius:admin:product_taxon:update + + + + + admin:product_taxon:show + sylius:admin:product_taxon:show + + + + GET /shop/product-taxons/{id} - shop:product_taxon:read + + shop:product_taxon:show + sylius:shop:product_taxon:show + + + + DELETE + /admin/product-taxons/{id} + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTranslation.xml index 8c37613741..816e07f585 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductTranslation.xml @@ -14,10 +14,6 @@ GET /admin/product-translations/{id} - - GET - /shop/product-translations/{id} - diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariant.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariant.xml index 1fc87a0e59..92fcd9457b 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariant.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariant.xml @@ -23,18 +23,27 @@ GET /admin/product-variants/{code} - admin:product_variant:read + + admin:product_variant:show + sylius:admin:product_variant:show + PUT /admin/product-variants/{code} - - admin:product_variant:read - - admin:product_variant:update + + admin:product_variant:update + sylius:admin:product_variant:update + + + + + admin:product_variant:show + sylius:admin:product_variant:show + @@ -42,9 +51,17 @@ GET /shop/product-variants/{code} - shop:product_variant:read + + shop:product_variant:show + sylius:shop:product_variant:show + + + + DELETE + /admin/product-variants/{code} + @@ -52,10 +69,15 @@ GET /admin/product-variants + sylius.api.product_variant_position_filter + sylius.api.product_variant_product_filter Sylius\Bundle\ApiBundle\Filter\Doctrine\ProductVariantCatalogPromotionFilter - admin:product_variant:read + + admin:product_variant:index + sylius:admin:product_variant:index + @@ -63,10 +85,16 @@ POST /admin/product-variants - admin:product_variant:create + + admin:product_variant:create + sylius:admin:product_variant:create + - admin:product_variant:create + + admin:product_variant:show + sylius:admin:product_variant:show + @@ -78,7 +106,10 @@ Sylius\Bundle\ApiBundle\Filter\Doctrine\ProductVariantOptionValueFilter - shop:product_variant:read + + shop:product_variant:index + sylius:shop:product_variant:index + @@ -93,8 +124,6 @@ string - string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariantTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariantTranslation.xml index 0945fe5b57..052ed8a570 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariantTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ProductVariantTranslation.xml @@ -12,12 +12,7 @@ GET - /admin/product-variant-translation/{id} - - - - GET - /shop/product-variant-translation/{id} + /admin/product-variant-translations/{id} diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml index 923c98350c..4aaa5dfd65 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Promotion.xml @@ -18,20 +18,78 @@ admin - sylius + + sylius + sylius_api + + + + DESC + POST - admin:promotion:create + + admin:promotion:create + sylius:admin:promotion:create + + + + + admin:promotion:show + sylius:admin:promotion:show + + + false + + + +Example configuration for `total_of_items_from_taxon` rule type: + +``` +{ + "type": "total_of_items_from_taxon", + "configuration": { + "channel-code": { + "taxon": "taxon-code", + "amount": int + } + } +} +``` + +Example configuration for `order_fixed_discount` action type: + +``` +{ + "type": "order_fixed_discount", + "configuration": { + "channel-code": { + "amount": int, + } + } +} +``` + GET - admin:promotion:read + + admin:promotion:index + sylius:admin:promotion:index + + + false + + + sylius.api.promotion_coupon_search_filter + sylius.api.promotion_order_filter + sylius.api.exists_filter.archived_at @@ -40,14 +98,106 @@ GET - admin:promotion:read + + admin:promotion:show + sylius:admin:promotion:show + + + false + + + PUT + + + admin:promotion:update + sylius:admin:promotion:update + + + + + admin:promotion:show + sylius:admin:promotion:show + + + false + + + +Example configuration for `total_of_items_from_taxon` rule type: + +``` +{ + "type": "total_of_items_from_taxon", + "configuration": { + "channel-code": { + "taxon": "taxon-code", + "amount": int + } + } +} +``` + +Example configuration for `order_fixed_discount` action type: + +``` +{ + "type": "order_fixed_discount", + "configuration": { + "channel-code": { + "amount": int, + } + } +} +``` + + + + + + PATCH + /promotions/{code}/archive + false + Sylius\Bundle\ApiBundle\Applicator\ArchivingPromotionApplicatorInterface::archive + + Archives Promotion + + + + admin:promotion:read + sylius:admin:promotion:read + + + + + PATCH + /promotions/{code}/restore + false + Sylius\Bundle\ApiBundle\Applicator\ArchivingPromotionApplicatorInterface::restore + + Restores Archived Promotion + + + + admin:promotion:read + sylius:admin:promotion:read + + + + DELETE + + + GET + /admin/promotions/{code}/coupons + + + @@ -59,9 +209,21 @@ - + + + + + + object + + + string + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionAction.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionAction.xml new file mode 100644 index 0000000000..7cb3cb7044 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionAction.xml @@ -0,0 +1,34 @@ + + + + + + + admin + + + + GET + ApiPlatform\Core\Action\NotFoundAction + false + false + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionCoupon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionCoupon.xml new file mode 100644 index 0000000000..52edf3d1ec --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionCoupon.xml @@ -0,0 +1,140 @@ + + + + + + + admin + + sylius + + + + GET + /promotion-coupons + + sylius.api.promotion_coupon_order_filter + Sylius\Bundle\ApiBundle\Filter\Doctrine\PromotionCouponPromotionFilter + + + + admin:promotion_coupon:index + sylius:admin:promotion_coupon:index + + + + DESC + + + + + POST + /promotion-coupons + + + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + + + + + admin:promotion_coupon:create + sylius:admin:promotion_coupon:create + + + + + + POST + input + /promotion-coupons/generate + Sylius\Bundle\ApiBundle\Command\Promotion\GeneratePromotionCoupon + + + admin:promotion_coupon:generate + sylius:admin:promotion_coupon:generate + + + + + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + + + + Generates promotion coupons + Generates promotion coupons + + + + + + + GET + /promotion-coupons/{code} + + + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + + + + + + PUT + /promotion-coupons/{code} + + + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + + + + + admin:promotion_coupon:update + sylius:admin:promotion_coupon:update + + + + + + DELETE + /promotion-coupons/{code} + + + + + + + + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionRule.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionRule.xml new file mode 100644 index 0000000000..0df12076c1 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionRule.xml @@ -0,0 +1,34 @@ + + + + + + + admin + + + + GET + ApiPlatform\Core\Action\NotFoundAction + false + false + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionTranslation.xml new file mode 100644 index 0000000000..67b944f395 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/PromotionTranslation.xml @@ -0,0 +1,34 @@ + + + + + + + sylius + + + + + + GET + /admin/promotion-translations/{id} + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Province.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Province.xml index ccc1fa430a..ede36c6ad6 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Province.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Province.xml @@ -16,8 +16,6 @@ xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd" > - admin - sylius @@ -25,25 +23,58 @@ GET + /admin/provinces/{code} - admin:province:read - - - - admin:province:update + + admin:province:show + sylius:admin:province:show + PUT + /admin/provinces/{code} + + + admin:province:update + sylius:admin:province:update + + + + + admin:province:show + sylius:admin:province:show + + + + + + GET + /shop/provinces/{code} + + + shop:province:show + sylius:shop:province:show + + + + + + + admin:province:show + sylius:admin:province:show + + + + + - - diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Shipment.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Shipment.xml index 6ea621e5c7..554d1bc068 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Shipment.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Shipment.xml @@ -30,7 +30,10 @@ sylius.api.search_shipment_filter - admin:shipment:read + + admin:shipment:index + sylius:admin:shipment:index + @@ -40,7 +43,10 @@ GET /admin/shipments/{id} - admin:shipment:read + + admin:shipment:show + sylius:admin:shipment:show + @@ -48,7 +54,10 @@ GET /shop/shipments/{id} - shop:shipment:read + + shop:shipment:show + sylius:shop:shipment:show + @@ -57,15 +66,48 @@ /admin/shipments/{id}/ship input Sylius\Bundle\ApiBundle\Command\Checkout\ShipShipment + false + 202 Ships Shipment - admin:shipment:update + + admin:shipment:update + sylius:admin:shipment:update + + + + + + POST + /admin/shipments/{id}/resend-confirmation-email + input + Sylius\Bundle\ApiBundle\Command\ResendShipmentConfirmationEmail + false + 202 + + Resends shipment confirmation email + + + + + admin:shipment:show + sylius:admin:shipment:show + + + + + + GET + /admin/shipments/{id}/adjustments + + + @@ -74,5 +116,8 @@ + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingCategory.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingCategory.xml index eb5ea7cb7a..90e21bda65 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingCategory.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingCategory.xml @@ -23,15 +23,29 @@ GET + /shipping-categories - admin:shipping_category:read + + admin:shipping_category:index + sylius:admin:shipping_category:index + POST + /shipping-categories - admin:shipping_category:create + + admin:shipping_category:create + sylius:admin:shipping_category:create + + + + + admin:shipping_category:show + sylius:admin:shipping_category:show + @@ -39,19 +53,34 @@ GET + /shipping-categories/{code} - admin:shipping_category:read + + admin:shipping_category:show + sylius:admin:shipping_category:show + PUT + /shipping-categories/{code} - admin:shipping_category:update + + admin:shipping_category:update + sylius:admin:shipping_category:update + + + + + admin:shipping_category:show + sylius:admin:shipping_category:show + + /shipping-categories/{code} DELETE diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethod.xml index cbd63c43dc..95a21ac53a 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethod.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethod.xml @@ -16,7 +16,7 @@ xsi:schemaLocation="https://api-platform.com/schema/metadata https://api-platform.com/schema/metadata/metadata-2.0.xsd" > - sylius + Sylius\Bundle\ShippingBundle\Validator\GroupsGenerator\ShippingMethodConfigurationGroupsGenerator ASC @@ -27,7 +27,10 @@ GET /admin/shipping-methods/{code} - admin:shipping_method:read + + admin:shipping_method:show + sylius:admin:shipping_method:show + @@ -35,18 +38,65 @@ GET /shop/shipping-methods/{code} - shop:shipping_method:read + + shop:shipping_method:show + sylius:shop:shipping_method:show + PUT /admin/shipping-methods/{code} - - admin:shipping_method:read - - admin:shipping_method:update + + admin:shipping_method:update + sylius:admin:shipping_method:update + + + + + admin:shipping_method:show + sylius:admin:shipping_method:show + + + + +Example configuration for `total_weight_greater_than_or_equal` rule type: + +``` +{ + "type": "total_weight_greater_than_or_equal", + "configuration": { + "weight": int + } +} +``` + +Example configuration for `order_total_greater_than_or_equal` rule type: + +``` +{ + "type": "order_total_greater_than_or_equal", + "configuration": { + "channel-code": [ + "amount": int, + ] + } +} +``` + +Example configuration for `flat_rate` shipping charges calculator: + +``` +"shippingChargesCalculator": "flat_rate", +"shippingChargesCalculatorConfiguration": { + "channel-code": [ + "amount": int, + ] +} +``` + @@ -64,7 +114,10 @@ Archives Shipping Method - admin:shipping_method:read + + admin:shipping_method:show + sylius:admin:shipping_method:show + @@ -77,7 +130,10 @@ Restores archived Shipping Method - admin:shipping_method:read + + admin:shipping_method:show + sylius:admin:shipping_method:show + @@ -92,7 +148,10 @@ Sylius\Bundle\ApiBundle\Filter\Doctrine\TranslationOrderNameAndLocaleFilter - admin:shipping_method:read + + admin:shipping_method:index + sylius:admin:shipping_method:index + @@ -104,7 +163,8 @@ - shop:shipping_method:read + shop:shipping_method:index + sylius:shop:shipping_method:index @@ -112,11 +172,55 @@ POST /admin/shipping-methods - - admin:shipping_method:read - - admin:shipping_method:create + + admin:shipping_method:create + sylius:admin:shipping_method:create + + + + + admin:shipping_method:show + sylius:admin:shipping_method:show + + + + +Example configuration for `total_weight_greater_than_or_equal` rule type: + +``` +{ + "type": "total_weight_greater_than_or_equal", + "configuration": { + "weight": int + } +} +``` + +Example configuration for `order_total_greater_than_or_equal` rule type: + +``` +{ + "type": "order_total_greater_than_or_equal", + "configuration": { + "channel-code": [ + "amount": int, + ] + } +} +``` + +Example configuration for `flat_rate` shipping charges calculator: + +``` +"shippingChargesCalculator": "flat_rate", +"shippingChargesCalculatorConfiguration": { + "channel-code": [ + "amount": int, + ] +} +``` + @@ -133,6 +237,7 @@ + object @@ -140,7 +245,6 @@ string string - string diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodRule.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodRule.xml new file mode 100644 index 0000000000..ddde51741f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodRule.xml @@ -0,0 +1,34 @@ + + + + + + + admin + + + + GET + ApiPlatform\Core\Action\NotFoundAction + false + false + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodTranslation.xml index c49f41a685..427d3e2b8e 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShippingMethodTranslation.xml @@ -25,11 +25,6 @@ GET /admin/shipping-method-translations/{id} - - - GET - /shop/shipping-method-translations/{id} - diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShopBillingData.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShopBillingData.xml index b29af59ed2..229010c11d 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShopBillingData.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShopBillingData.xml @@ -26,11 +26,25 @@ GET - admin:shop_billing_data:read + + admin:shop_billing_data:show + sylius:admin:shop_billing_data:show + + + + + + admin:shop_billing_data:read + sylius:admin:shop_billing_data:show + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShopUser.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShopUser.xml new file mode 100644 index 0000000000..dea9fb626c --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ShopUser.xml @@ -0,0 +1,35 @@ + + + + + + + admin + + + + GET + false + ApiPlatform\Core\Action\NotFoundAction + false + false + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxCategory.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxCategory.xml index 8638197ef1..6d7964c553 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxCategory.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxCategory.xml @@ -23,15 +23,29 @@ GET + /tax-categories - admin:tax_category:read + + admin:tax_category:index + sylius:admin:tax_category:index + POST + /tax-categories - admin:tax_category:create + + admin:tax_category:create + sylius:admin:tax_category:create + + + + + admin:tax_category:show + sylius:admin:tax_category:show + @@ -39,27 +53,42 @@ GET + /tax-categories/{code} - admin:tax_category:read + + admin:tax_category:show + sylius:admin:tax_category:show + PUT + /tax-categories/{code} - admin:tax_category:update + + admin:tax_category:update + sylius:admin:tax_category:update + + + + + admin:tax_category:show + sylius:admin:tax_category:show + DELETE + /tax-categories/{code} + - diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxRate.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxRate.xml new file mode 100644 index 0000000000..2686ab4210 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxRate.xml @@ -0,0 +1,93 @@ + + + + + + + admin + sylius + + + + GET + /tax-rates/{code} + + + admin:tax_rate:show + sylius:admin:tax_rate:show + + + + + + PUT + /tax-rates/{code} + + + admin:tax_rate:update + sylius:admin:tax_rate:update + + + + + admin:tax_rate:show + sylius:admin:tax_rate:show + + + + + + DELETE + /tax-rates/{code} + + + + + + GET + /tax-rates + + sylius.api.tax_rate.date_filter + + + + admin:tax_rate:index + sylius:admin:tax_rate:index + + + + + + POST + /tax-rates + + + admin:tax_rate:create + sylius:admin:tax_rate:create + + + + + admin:tax_rate:show + sylius:admin:tax_rate:show + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Taxon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Taxon.xml index 2a2940b04d..14e7aa87df 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Taxon.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Taxon.xml @@ -12,7 +12,10 @@ GET /admin/taxons - admin:taxon:read + + admin:taxon:index + sylius:admin:taxon:index + @@ -20,7 +23,16 @@ POST /admin/taxons - admin:taxon:create + + admin:taxon:create + sylius:admin:taxon:create + + + + + admin:taxon:show + sylius:admin:taxon:show + @@ -28,7 +40,10 @@ GET /shop/taxons - shop:taxon:read + + shop:taxon:index + sylius:shop:taxon:index + @@ -38,7 +53,10 @@ GET /admin/taxons/{code} - admin:taxon:read + + admin:taxon:show + sylius:admin:taxon:show + @@ -46,25 +64,68 @@ PUT /admin/taxons/{code} - admin:taxon:update + + admin:taxon:update + sylius:admin:taxon:update + + + + admin:taxon:show + sylius:admin:taxon:show + + + + + + DELETE + /admin/taxons/{code} GET /shop/taxons/{code} - shop:taxon:read + + shop:taxon:show + sylius:shop:taxon:show + + + + GET + /admin/taxons/{code}/images + + Retrieves the collection of TaxonImage resources for a given Taxon. + + + + + - + + + + object + + + string + string + string + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxonImage.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxonImage.xml new file mode 100644 index 0000000000..cf7eadba5d --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxonImage.xml @@ -0,0 +1,117 @@ + + + + + sylius + + + + GET + /admin/taxon-images + + + admin:taxon_image:index + sylius:admin:taxon_image:index + + + + + + POST + /admin/taxons/{code}/images + Sylius\Bundle\ApiBundle\Controller\UploadTaxonImageAction + false + + + + code + path + true + + string + + + + + + + + object + + + string + binary + + + string + + + + + + + + + + admin:taxon_image:show + sylius:admin:taxon_image:show + + + + + + + + GET + /admin/taxon-images/{id} + + + admin:taxon_image:show + sylius:admin:taxon_image:show + + + + + + PUT + /admin/taxon-images/{id} + + + admin:taxon_image:update + sylius:admin:taxon_image:update + + + + + admin:taxon_image:show + sylius:admin:taxon_image:show + + + + + + DELETE + /admin/taxon-images/{id} + + + + + + + + admin:taxon_image:show + sylius:admin:taxon_image:show + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxonTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxonTranslation.xml index d72de3ae7b..b27450627c 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxonTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/TaxonTranslation.xml @@ -7,18 +7,7 @@ sylius - - - GET - /admin/taxon-translations - - - admin:taxon:read - admin:taxon_translation:read - - - - + @@ -26,19 +15,10 @@ /admin/taxon-translations/{id} - admin:taxon:read - admin:taxon_translation:read - - - - - - GET - /shop/taxon-translations/{id} - - - admin:taxon:read - shop:taxon_translation:read + admin:taxon:show + sylius:admin:taxon:show + admin:taxon_translation:show + sylius:admin:taxon_translation:show @@ -48,5 +28,6 @@ + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/VerifyCustomerAccount.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/VerifyCustomerAccount.xml index 83b7297d4b..f7f8532f48 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/VerifyCustomerAccount.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/VerifyCustomerAccount.xml @@ -28,7 +28,10 @@ Sylius\Bundle\ApiBundle\Command\Account\ResendVerificationEmail 202 - shop:resend_verification_email:create + + shop:resend_verification_email:create + sylius:shop:resend_verification_email:create + Resends verification email @@ -44,7 +47,10 @@ Sylius\Bundle\ApiBundle\Command\Account\VerifyCustomerAccount 202 - shop:account_verification:update + + shop:account_verification:update + sylius:shop:account_verification:update + Verifies Customer account diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Zone.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Zone.xml index 4610127549..1bda8086b2 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Zone.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/Zone.xml @@ -25,7 +25,10 @@ GET /zones - admin:zone:read + + admin:zone:index + sylius:admin:zone:index + @@ -33,7 +36,16 @@ POST /zones - admin:zone:create + + admin:zone:create + sylius:admin:zone:create + + + + + admin:zone:show + sylius:admin:zone:show + @@ -43,7 +55,10 @@ GET /zones/{code} - admin:zone:read + + admin:zone:show + sylius:admin:zone:show + @@ -51,7 +66,16 @@ PUT /zones/{code} - admin:zone:update + + admin:zone:update + sylius:admin:zone:update + + + + + admin:zone:show + sylius:admin:zone:show + @@ -61,6 +85,12 @@ + + + GET + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ZoneMember.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ZoneMember.xml index 5e314fd24c..506d105bb9 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ZoneMember.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/api_resources/ZoneMember.xml @@ -27,11 +27,25 @@ GET /zone-members/{id} - admin:zone_member:read + + admin:zone_member:show + sylius:admin:zone_member:show + + + + + + admin:zone_member:show + sylius:admin:zone_member:show + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml b/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml index febb5729dc..e8df4bb5ff 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/app/config.yaml @@ -4,10 +4,19 @@ parameters: env(SYLIUS_API_AUTHORIZATION_HEADER): Authorization sylius.api.authorization_header: "%env(resolve:SYLIUS_API_AUTHORIZATION_HEADER)%" + sylius.api.doctrine_extension.order_shop_user_item.filter_cart.allowed_non_get_operations: + - "shop_select_payment_method" + - "shop_account_change_payment_method" + sylius.api.doctrine_extension.order_visitor_item.filter_cart.allowed_non_get_operations: + - "shop_select_payment_method" sylius.api.paths_to_hide: - "%sylius.security.new_api_route%/admin/catalog-promotion-actions/{id}" - "%sylius.security.new_api_route%/admin/catalog-promotion-scopes/{id}" - "%sylius.security.new_api_route%/admin/channel-pricings/{id}" + - "%sylius.security.new_api_route%/admin/promotion-actions/{id}" + - "%sylius.security.new_api_route%/admin/promotion-rules/{id}" + - "%sylius.security.new_api_route%/admin/shipping-method-rules/{id}" + - "%sylius.security.new_api_route%/admin/shop-users/{id}" sylius.security.new_api_route: "/api/v2" sylius.security.new_api_regex: "^%sylius.security.new_api_route%" sylius.security.new_api_admin_route: "%sylius.security.new_api_route%/admin" @@ -23,6 +32,8 @@ sylius_api: '%sylius.model.product.class%': operations: shop_get: ~ + order_states_to_filter_out: + - !php/const Sylius\Component\Core\Model\OrderInterface::STATE_CART api_platform: patch_formats: @@ -43,15 +54,35 @@ api_platform: Symfony\Component\Serializer\Exception\ExceptionInterface: 400 # Sylius exception to status code mapping - SM\SMException: 422 + Sylius\Abstraction\StateMachine\Exception\StateMachineExecutionException: 422 Sylius\Bundle\ApiBundle\Exception\CannotRemoveCurrentlyLoggedInUser: 422 + Sylius\Bundle\ApiBundle\Exception\ChannelCannotBeRemoved: 422 + Sylius\Bundle\ApiBundle\Exception\ChannelPricingChannelCodeMismatchException: 422 + Sylius\Bundle\ApiBundle\Exception\InvalidProductAttributeValueTypeException: 422 + Sylius\Bundle\ApiBundle\Exception\InvalidRequestArgumentException: 400 + Sylius\Bundle\ApiBundle\Exception\LocaleIsUsedException: 422 Sylius\Bundle\ApiBundle\Exception\OrderItemNotFoundException: 422 Sylius\Bundle\ApiBundle\Exception\OrderNoLongerEligibleForPromotion: 422 + Sylius\Bundle\ApiBundle\Exception\PaymentMethodCannotBeRemoved: 422 + Sylius\Bundle\ApiBundle\Exception\ProductAttributeCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\ProductCannotBeRemoved: 422 + Sylius\Bundle\ApiBundle\Exception\ProductVariantCannotBeRemoved: 422 + Sylius\Bundle\ApiBundle\Exception\PromotionCouponCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\ProvinceCannotBeRemoved: 422 Sylius\Bundle\ApiBundle\Exception\ShippingMethodCannotBeRemoved: 422 + Sylius\Bundle\ApiBundle\Exception\StateMachineTransitionFailedException: 422 + Sylius\Bundle\ApiBundle\Exception\TaxonCannotBeRemoved: 422 + Sylius\Bundle\ApiBundle\Exception\TranslationInDefaultLocaleCannotBeRemoved: 422 + Sylius\Bundle\ApiBundle\Exception\TranslationLocaleMismatchException: 422 Sylius\Bundle\ApiBundle\Exception\ZoneCannotBeRemoved: 422 + Sylius\Bundle\ApiBundle\Exception\CannotRemoveMenuTaxonException: 409 + Sylius\Bundle\ApiBundle\Exception\PromotionCannotBeRemoved: 422 + Sylius\Bundle\UserBundle\Exception\UserNotFoundException: 404 + Sylius\Component\Promotion\Exception\FailedGenerationException: 422 + Symfony\Component\Serializer\Exception\UnexpectedValueException: 400 Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException: 400 + Sylius\Bundle\ApiBundle\CommandHandler\Checkout\Exception\OrderTotalHasChangedException: 409 + Sylius\Bundle\ApiBundle\Serializer\Exception\InvalidAmountTypeException: 400 collection: pagination: client_items_per_page: true diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/integrations/swagger.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/integrations/swagger.xml index 5a409dc6ff..c8f7b4a681 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/integrations/swagger.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/integrations/swagger.xml @@ -16,121 +16,95 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd" > - + - - - - - - - %api_platform.title% - %api_platform.description% - %api_platform.version% - %api_platform.formats% - %api_platform.oauth.enabled% - %api_platform.oauth.clientId% - %api_platform.oauth.clientSecret% - %api_platform.oauth.type% - %api_platform.oauth.flow% - %api_platform.oauth.tokenUrl% - %api_platform.oauth.authorizationUrl% - %api_platform.oauth.scopes% - %api_platform.show_webby% - %api_platform.enable_swagger_ui% - %api_platform.enable_re_doc% - %api_platform.graphql.enabled% - %api_platform.graphql.graphiql.enabled% - %api_platform.graphql.graphql_playground.enabled% - %api_platform.swagger.versions% + + + - - + + + + + + %sylius.security.new_api_route% + - - - %sylius.security.new_api_route% + + %sylius.api.paths_to_hide% + - - + + + - - + + + + + %sylius.security.new_api_route% + - - + %sylius.security.new_api_route% + - - + + %sylius.security.new_api_route% + - - + + - - - %sylius.api.paths_to_hide% + + %sylius.security.new_api_route% + %sylius.shipping_method_rules% + %sylius.shipping_calculators% + - - - + + %sylius.security.new_api_route% + + + + + + + + + %sylius.security.new_api_route% + + %sylius_core.orders_statistics.intervals_map% + + + + + %sylius.security.new_api_route% + %sylius.promotion_actions% + %sylius.promotion_rules% + + + + + %sylius.security.new_api_route% + %sylius.model.adjustment.class% + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/routing.yml b/src/Sylius/Bundle/ApiBundle/Resources/config/routing.yml index 2a9ad9d1cc..dd8c26ba63 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/routing.yml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/routing.yml @@ -10,6 +10,12 @@ sylius_api_admin_authentication_token: path: /admin/authentication-token methods: ['POST'] +sylius_api_admin_statistics: + path: /admin/statistics + methods: ['GET'] + defaults: + _controller: Sylius\Bundle\ApiBundle\Controller\GetStatisticsAction + sylius_api_shop_authentication_token: path: /shop/authentication-token methods: ['POST'] diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Address.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Address.xml index 03176c1455..b4a3c80021 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Address.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Address.xml @@ -17,110 +17,276 @@ > - admin:address:read - shop:address:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show - admin:address:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show - admin:address:read - admin:order:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + admin:address:update + sylius:admin:address:update + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show shop:address:create - shop:address:read + sylius:shop:address:create + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show shop:address:update + sylius:shop:address:update shop:cart:update - shop:cart:read - shop:order:account:read + sylius:shop:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:address:read - admin:order:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + admin:address:update + sylius:admin:address:update + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show shop:address:create - shop:address:read + sylius:shop:address:create + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show shop:address:update + sylius:shop:address:update shop:cart:update - shop:cart:read - shop:order:account:read + sylius:shop:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:address:read - admin:order:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + admin:address:update + sylius:admin:address:update + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show shop:address:create - shop:address:read + sylius:shop:address:create + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show shop:address:update + sylius:shop:address:update shop:cart:update - shop:cart:read - shop:order:account:read + sylius:shop:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:address:read - admin:order:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + admin:address:update + sylius:admin:address:update + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show shop:address:create - shop:address:read + sylius:shop:address:create + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show shop:address:update + sylius:shop:address:update shop:cart:update - shop:cart:read - shop:order:account:read + sylius:shop:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:address:read - admin:order:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + admin:address:update + sylius:admin:address:update + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show shop:address:create - shop:address:read + sylius:shop:address:create + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show shop:address:update + sylius:shop:address:update shop:cart:update - shop:cart:read - shop:order:account:read + sylius:shop:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:address:read - admin:order:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + admin:address:update + sylius:admin:address:update + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show shop:address:create - shop:address:read + sylius:shop:address:create + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show shop:address:update + sylius:shop:address:update shop:cart:update - shop:cart:read - shop:order:account:read + sylius:shop:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:address:read - admin:order:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + admin:address:update + sylius:admin:address:update + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show shop:address:create - shop:address:read + sylius:shop:address:create + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show shop:address:update + sylius:shop:address:update shop:cart:update - shop:cart:read - shop:order:account:read + sylius:shop:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:address:read - admin:order:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + admin:address:update + sylius:admin:address:update + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show shop:address:create - shop:address:read + sylius:shop:address:create + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show shop:address:update + sylius:shop:address:update shop:cart:update - shop:cart:read - shop:order:account:read + sylius:shop:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:address:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + admin:address:update + sylius:admin:address:update shop:address:create - shop:address:read + sylius:shop:address:create + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show shop:address:update + sylius:shop:address:update shop:cart:update - shop:cart:read - shop:order:account:read + sylius:shop:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:address:read - admin:order:read + admin:address:index + sylius:admin:address:index + admin:address:show + sylius:admin:address:show + admin:address:update + sylius:admin:address:update + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show shop:address:create - shop:address:read + sylius:shop:address:create + shop:address:index + sylius:shop:address:index + shop:address:show + sylius:shop:address:show shop:address:update + sylius:shop:address:update shop:cart:update - shop:cart:read - shop:order:account:read + sylius:shop:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AddressLogEntry.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AddressLogEntry.xml new file mode 100644 index 0000000000..f656a6d3a7 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AddressLogEntry.xml @@ -0,0 +1,36 @@ + + + + + + + + admin:address:log_entry:show + sylius:admin:address:log_entry:show + + + admin:address:log_entry:show + sylius:admin:address:log_entry:show + + + admin:address:log_entry:show + sylius:admin:address:log_entry:show + + + admin:address:log_entry:show + sylius:admin:address:log_entry:show + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Adjustment.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Adjustment.xml index 9338271e7b..49e00d2ed3 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Adjustment.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Adjustment.xml @@ -17,53 +17,143 @@ > - admin:adjustment:read - shop:adjustment:read - shop:cart:read + admin:adjustment:index + sylius:admin:adjustment:index + admin:adjustment:show + sylius:admin:adjustment:show + shop:adjustment:index + sylius:shop:adjustment:index + shop:adjustment:show + sylius:shop:adjustment:show + shop:cart:show + sylius:shop:cart:show - admin:adjustment:read - shop:adjustment:read - shop:cart:read - + admin:adjustment:index + sylius:admin:adjustment:index + admin:adjustment:show + sylius:admin:adjustment:show + shop:adjustment:index + sylius:shop:adjustment:index + shop:adjustment:show + sylius:shop:adjustment:show + shop:cart:show + sylius:shop:cart:show + sylius:admin:order_item:index + admin:order_item_unit:index + sylius:admin:order_item_unit:index + admin:shipment:index + sylius:admin:shipment:index + - admin:adjustment:read - shop:adjustment:read - shop:cart:read - + admin:adjustment:index + sylius:admin:adjustment:index + admin:adjustment:show + sylius:admin:adjustment:show + shop:adjustment:index + sylius:shop:adjustment:index + shop:adjustment:show + sylius:shop:adjustment:show + shop:cart:show + sylius:shop:cart:show + - - admin:adjustment:read - shop:adjustment:read - shop:cart:read - + + admin:adjustment:index + sylius:admin:adjustment:index + admin:adjustment:show + sylius:admin:adjustment:show + shop:adjustment:index + sylius:shop:adjustment:index + shop:adjustment:show + sylius:shop:adjustment:show + shop:cart:show + sylius:shop:cart:show + sylius:admin:order_item:index + admin:order_item_unit:index + sylius:admin:order_item_unit:index + admin:shipment:index + sylius:admin:shipment:index + - - admin:adjustment:read - shop:adjustment:read - + + admin:adjustment:index + sylius:admin:adjustment:index + admin:adjustment:show + sylius:admin:adjustment:show + shop:adjustment:index + sylius:shop:adjustment:index + shop:adjustment:show + sylius:shop:adjustment:show + - - admin:adjustment:read - shop:adjustment:read - + + admin:adjustment:index + sylius:admin:adjustment:index + admin:adjustment:show + sylius:admin:adjustment:show + shop:adjustment:index + sylius:shop:adjustment:index + shop:adjustment:show + sylius:shop:adjustment:show + - - admin:adjustment:read - shop:adjustment:read - + + admin:adjustment:index + sylius:admin:adjustment:index + admin:adjustment:show + sylius:admin:adjustment:show + shop:adjustment:index + sylius:shop:adjustment:index + shop:adjustment:show + sylius:shop:adjustment:show + - - admin:adjustment:read - shop:adjustment:read - shop:cart:read - + + admin:adjustment:index + sylius:admin:adjustment:index + admin:adjustment:show + sylius:admin:adjustment:show + shop:adjustment:index + sylius:shop:adjustment:index + shop:adjustment:show + sylius:shop:adjustment:show + - - admin:adjustment:read - shop:adjustment:read - + + admin:adjustment:index + sylius:admin:adjustment:index + admin:adjustment:show + sylius:admin:adjustment:show + shop:adjustment:index + sylius:shop:adjustment:index + shop:adjustment:show + sylius:shop:adjustment:show + shop:cart:show + sylius:shop:cart:show + sylius:admin:order_item:index + admin:order_item_unit:index + sylius:admin:order_item_unit:index + admin:shipment:index + sylius:admin:shipment:index + + + + admin:adjustment:index + sylius:admin:adjustment:index + admin:adjustment:show + sylius:admin:adjustment:show + shop:adjustment:index + sylius:shop:adjustment:index + shop:adjustment:show + sylius:shop:adjustment:show + sylius:admin:order_item:index + admin:order_item_unit:index + sylius:admin:order_item_unit:index + admin:shipment:index + sylius:admin:shipment:index + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AdminUser.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AdminUser.xml index 1f3526f6b3..1b56e2b05c 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AdminUser.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AdminUser.xml @@ -17,56 +17,106 @@ > - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show admin:admin_user:create + sylius:admin:admin_user:create admin:admin_user:update + sylius:admin:admin_user:update - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show admin:admin_user:create + sylius:admin:admin_user:create admin:admin_user:update + sylius:admin:admin_user:update admin:admin_user:create + sylius:admin:admin_user:create admin:admin_user:update + sylius:admin:admin_user:update - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show admin:admin_user:create + sylius:admin:admin_user:create admin:admin_user:update + sylius:admin:admin_user:update - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show admin:admin_user:create + sylius:admin:admin_user:create admin:admin_user:update + sylius:admin:admin_user:update - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show admin:admin_user:create + sylius:admin:admin_user:create admin:admin_user:update + sylius:admin:admin_user:update - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show admin:admin_user:create + sylius:admin:admin_user:create admin:admin_user:update + sylius:admin:admin_user:update - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show - admin:admin_user:read + admin:admin_user:index + sylius:admin:admin_user:index + admin:admin_user:show + sylius:admin:admin_user:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AvatarImage.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AvatarImage.xml index b776d8e280..1c79716b7f 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AvatarImage.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/AvatarImage.xml @@ -17,13 +17,16 @@ > - admin:avatar_image:read + admin:avatar_image:show + sylius:admin:avatar_image:show - admin:avatar_image:read + admin:avatar_image:show + sylius:admin:avatar_image:show - admin:avatar_image:read + admin:avatar_image:show + sylius:admin:avatar_image:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotion.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotion.xml index bd09f8f738..d78f462f22 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotion.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotion.xml @@ -13,83 +13,155 @@ > - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update - shop:catalog_promotion:read + sylius:admin:catalog_promotion:update + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + shop:catalog_promotion:show + sylius:shop:catalog_promotion:show - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update - admin:catalog_promotion:read + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show - shop:catalog_promotion:read + shop:catalog_promotion:show + sylius:shop:catalog_promotion:show - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionAction.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionAction.xml index c134f56fd6..3b1445b28f 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionAction.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionAction.xml @@ -13,17 +13,30 @@ > - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionScope.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionScope.xml index 1c9a2117eb..ec160d963d 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionScope.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionScope.xml @@ -17,17 +17,30 @@ > - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionTranslation.xml index e9932c3afd..dc8db41130 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CatalogPromotionTranslation.xml @@ -17,24 +17,39 @@ > - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update - admin:catalog_promotion:read + admin:catalog_promotion:index + sylius:admin:catalog_promotion:index + admin:catalog_promotion:show + sylius:admin:catalog_promotion:show admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update admin:catalog_promotion:create + sylius:admin:catalog_promotion:create admin:catalog_promotion:update + sylius:admin:catalog_promotion:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Channel.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Channel.xml index f9dbee375c..b655e03e52 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Channel.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Channel.xml @@ -17,95 +17,256 @@ > - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create - shop:channel:read + sylius:admin:channel:create + shop:channel:index + sylius:shop:channel:index + shop:channel:show + sylius:shop:channel:show - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create - shop:channel:read + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + shop:channel:index + sylius:shop:channel:index + shop:channel:show + sylius:shop:channel:show + + + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show + admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create - shop:channel:read + sylius:admin:channel:create + shop:channel:index + sylius:shop:channel:index + shop:channel:show + sylius:shop:channel:show - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create - shop:channel:read + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + shop:channel:index + sylius:shop:channel:index + shop:channel:show + sylius:shop:channel:show - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create - shop:channel:read + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + shop:channel:index + sylius:shop:channel:index + shop:channel:show + sylius:shop:channel:show - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create - shop:channel:read + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + shop:channel:index + sylius:shop:channel:index + shop:channel:show + sylius:shop:channel:show - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + admin:shop_billing_data:show + sylius:admin:shop_billing_data:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update - admin:channel:read + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + + + admin:channel:index + sylius:admin:channel:index + admin:channel:show + sylius:admin:channel:show + admin:channel:create + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPriceHistoryConfig.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPriceHistoryConfig.xml new file mode 100644 index 0000000000..c1728ea5ee --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPriceHistoryConfig.xml @@ -0,0 +1,48 @@ + + + + + + + + admin:channel_price_history_config:show + sylius:admin:channel_price_history_config:show + + + admin:channel:create + sylius:admin:channel:create + admin:channel_price_history_config:update + sylius:admin:channel_price_history_config:update + admin:channel_price_history_config:show + sylius:admin:channel_price_history_config:show + + + admin:channel:create + sylius:admin:channel:create + admin:channel_price_history_config:update + sylius:admin:channel_price_history_config:update + admin:channel_price_history_config:show + sylius:admin:channel_price_history_config:show + + + admin:channel:create + sylius:admin:channel:create + admin:channel_price_history_config:update + sylius:admin:channel_price_history_config:update + admin:channel_price_history_config:show + sylius:admin:channel_price_history_config:show + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricing.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricing.xml index fbbf580aba..47f7e6d7cd 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricing.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricing.xml @@ -16,31 +16,58 @@ xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" > - - admin:product_variant:read - - - admin:product_variant:read admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update - admin:product_variant:read - admin:product_variant:update + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update - admin:product_variant:read - admin:product_variant:update + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update - admin:product_variant:read - admin:product_variant:update + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricingLogEntry.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricingLogEntry.xml new file mode 100644 index 0000000000..515d02d0e0 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ChannelPricingLogEntry.xml @@ -0,0 +1,44 @@ + + + + + + + + admin:channel_pricing_log_entry:index + sylius:admin:channel_pricing_log_entry:index + admin:channel_pricing_log_entry:show + sylius:admin:channel_pricing_log_entry:show + + + admin:channel_pricing_log_entry:index + sylius:admin:channel_pricing_log_entry:index + admin:channel_pricing_log_entry:show + sylius:admin:channel_pricing_log_entry:show + + + admin:channel_pricing_log_entry:index + sylius:admin:channel_pricing_log_entry:index + admin:channel_pricing_log_entry:show + sylius:admin:channel_pricing_log_entry:show + + + admin:channel_pricing_log_entry:index + sylius:admin:channel_pricing_log_entry:index + admin:channel_pricing_log_entry:show + sylius:admin:channel_pricing_log_entry:show + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ChangePasswordShopUser.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ChangePasswordShopUser.xml index 6716b47852..8efbe5b2eb 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ChangePasswordShopUser.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ChangePasswordShopUser.xml @@ -18,12 +18,15 @@ shop:customer:password:update + sylius:shop:customer:password:update shop:customer:password:update + sylius:shop:customer:password:update shop:customer:password:update + sylius:shop:customer:password:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ChangePaymentMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ChangePaymentMethod.xml index 7395dc6f2b..9bcc55f209 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ChangePaymentMethod.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ChangePaymentMethod.xml @@ -18,6 +18,7 @@ shop:order:account:change_payment_method + sylius:shop:order:account:change_payment_method diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/RegisterShopUser.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/RegisterShopUser.xml index e386e27c47..504e30f7bf 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/RegisterShopUser.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/RegisterShopUser.xml @@ -18,18 +18,23 @@ shop:customer:create + sylius:shop:customer:create shop:customer:create + sylius:shop:customer:create shop:customer:create + sylius:shop:customer:create shop:customer:create + sylius:shop:customer:create shop:customer:create + sylius:shop:customer:create diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/RequestResetPasswordToken.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/RequestResetPasswordToken.xml index 4b014eddcb..a9d1961229 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/RequestResetPasswordToken.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/RequestResetPasswordToken.xml @@ -18,9 +18,11 @@ shop:reset_password:create + sylius:shop:reset_password:create shop:reset_password:create + sylius:shop:reset_password:create diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ResendVerificationEmail.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ResendVerificationEmail.xml deleted file mode 100644 index 6f19327f08..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ResendVerificationEmail.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ResetPassword.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ResetPassword.xml index fc35de897a..77b60fd9a6 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ResetPassword.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/ResetPassword.xml @@ -18,9 +18,11 @@ shop:reset_password:update + sylius:shop:reset_password:update shop:reset_password:update + sylius:shop:reset_password:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/AddProductReview.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/AddProductReview.xml index 896312a3a2..c51241a165 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/AddProductReview.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/AddProductReview.xml @@ -18,23 +18,43 @@ shop:product_review:create - shop:product_review:read + sylius:shop:product_review:create + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show shop:product_review:create - shop:product_review:read + sylius:shop:product_review:create + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show shop:product_review:create - shop:product_review:read + sylius:shop:product_review:create + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show shop:product_review:create - shop:product_review:read + sylius:shop:product_review:create + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show shop:product_review:create - shop:product_review:read + sylius:shop:product_review:create + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/AddItemToCart.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/AddItemToCart.xml index 73295dc7fb..0ef538c52f 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/AddItemToCart.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/AddItemToCart.xml @@ -18,9 +18,11 @@ shop:cart:add_item + sylius:shop:cart:add_item shop:cart:add_item + sylius:shop:cart:add_item diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/ChangeItemQuantityInCart.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/ChangeItemQuantityInCart.xml index 16f637daa6..69b9b3aee5 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/ChangeItemQuantityInCart.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/ChangeItemQuantityInCart.xml @@ -18,6 +18,7 @@ shop:cart:change_quantity + sylius:shop:cart:change_quantity diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/RemoveItemFromCart.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/RemoveItemFromCart.xml index 312b15ac6d..a0c9f3d498 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/RemoveItemFromCart.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Cart/RemoveItemFromCart.xml @@ -18,6 +18,7 @@ shop:cart:remove_item + sylius:shop:cart:remove_item diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ChoosePaymentMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ChoosePaymentMethod.xml index 811a89d0f5..7d3eaf315f 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ChoosePaymentMethod.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ChoosePaymentMethod.xml @@ -18,6 +18,7 @@ shop:cart:select_payment_method + sylius:shop:cart:select_payment_method diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ChooseShippingMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ChooseShippingMethod.xml index e6c055931b..d4214a3c93 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ChooseShippingMethod.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ChooseShippingMethod.xml @@ -18,6 +18,7 @@ shop:cart:select_shipping_method + sylius:shop:cart:select_shipping_method diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/CompleteOrder.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/CompleteOrder.xml index 0c49bb7d4e..947863789d 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/CompleteOrder.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/CompleteOrder.xml @@ -18,6 +18,7 @@ shop:cart:complete + sylius:shop:cart:complete diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ShipShipment.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ShipShipment.xml index 3f7cf3e741..82af6cb414 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ShipShipment.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/ShipShipment.xml @@ -18,6 +18,7 @@ admin:shipment:update + sylius:admin:shipment:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/UpdateCart.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/UpdateCart.xml index dff2e138c8..991539b871 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/UpdateCart.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Checkout/UpdateCart.xml @@ -18,15 +18,19 @@ shop:cart:update + sylius:shop:cart:update shop:cart:update + sylius:shop:cart:update shop:cart:update + sylius:shop:cart:update shop:cart:update + sylius:shop:cart:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Promotion/GeneratePromotionCoupon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Promotion/GeneratePromotionCoupon.xml new file mode 100644 index 0000000000..9ad61e4013 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Promotion/GeneratePromotionCoupon.xml @@ -0,0 +1,48 @@ + + + + + + + + admin:promotion_coupon:generate + sylius:admin:promotion_coupon:generate + + + admin:promotion_coupon:generate + sylius:admin:promotion_coupon:generate + + + admin:promotion_coupon:generate + sylius:admin:promotion_coupon:generate + + + admin:promotion_coupon:generate + sylius:admin:promotion_coupon:generate + + + admin:promotion_coupon:generate + sylius:admin:promotion_coupon:generate + + + admin:promotion_coupon:generate + sylius:admin:promotion_coupon:generate + + + admin:promotion_coupon:generate + sylius:admin:promotion_coupon:generate + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/SendContactRequest.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/SendContactRequest.xml index a7ce9bb40c..0a3e18a7d9 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/SendContactRequest.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/SendContactRequest.xml @@ -18,9 +18,11 @@ shop:contact_request:create + sylius:shop:contact_request:create shop:contact_request:create + sylius:shop:contact_request:create diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Country.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Country.xml index 0b61c03b3f..6e1d2ea6a9 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Country.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Country.xml @@ -17,27 +17,56 @@ > - admin:country:read + admin:country:index + sylius:admin:country:index + admin:country:show + sylius:admin:country:show - admin:country:read + admin:country:index + sylius:admin:country:index + admin:country:show + sylius:admin:country:show admin:country:create - shop:country:read + sylius:admin:country:create + shop:country:index + sylius:shop:country:index + shop:country:show + sylius:shop:country:show - admin:country:read - shop:country:read + admin:country:index + sylius:admin:country:index + admin:country:show + sylius:admin:country:show + shop:country:index + sylius:shop:country:index + shop:country:show + sylius:shop:country:show - admin:country:read + admin:country:index + sylius:admin:country:index + admin:country:show + sylius:admin:country:show admin:country:create + sylius:admin:country:create admin:country:update + sylius:admin:country:update - admin:country:read + admin:country:index + sylius:admin:country:index + admin:country:show + sylius:admin:country:show admin:country:create + sylius:admin:country:create admin:country:update - shop:country:read + sylius:admin:country:update + shop:country:index + sylius:shop:country:index + shop:country:show + sylius:shop:country:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Currency.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Currency.xml index d2300f88a6..aef33a2ea2 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Currency.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Currency.xml @@ -16,17 +16,27 @@ xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" > - - admin:currency:read - shop:currency:read + + admin:currency:index + sylius:admin:currency:index + admin:currency:show + sylius:admin:currency:show + admin:currency:create + sylius:admin:currency:create + shop:currency:index + sylius:shop:currency:index + shop:currency:show + sylius:shop:currency:show - - admin:currency:read - shop:currency:read - - admin:currency:read - shop:currency:read + admin:currency:index + sylius:admin:currency:index + admin:currency:show + sylius:admin:currency:show + shop:currency:index + sylius:shop:currency:index + shop:currency:show + sylius:shop:currency:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Customer.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Customer.xml index df108e2e77..791b1631f8 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Customer.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Customer.xml @@ -17,51 +17,166 @@ > - admin:customer:read + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show - admin:customer:read - shop:customer:read + admin:customer:create + sylius:admin:customer:create + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + admin:customer:update + sylius:admin:customer:update + shop:customer:show + sylius:shop:customer:show shop:customer:update + sylius:shop:customer:update - admin:customer:read - shop:customer:read + admin:customer:create + sylius:admin:customer:create + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + admin:customer:update + sylius:admin:customer:update + shop:customer:show + sylius:shop:customer:show shop:customer:update + sylius:shop:customer:update - admin:customer:read - shop:customer:read + admin:customer:create + sylius:admin:customer:create + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + admin:customer:update + sylius:admin:customer:update + shop:customer:show + sylius:shop:customer:show shop:customer:update + sylius:shop:customer:update - admin:customer:read - shop:customer:read + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + shop:customer:show + sylius:shop:customer:show - admin:customer:read - shop:customer:read - shop:customer:update - - - - shop:customer:read + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + shop:customer:show + sylius:shop:customer:show shop:customer:update + sylius:shop:customer:update - shop:customer:read + admin:customer:create + sylius:admin:customer:create + admin:customer:update + sylius:admin:customer:update + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + shop:customer:show + sylius:shop:customer:show + + + + admin:customer:create + sylius:admin:customer:create + admin:customer:update + sylius:admin:customer:update + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show - admin:customer:read - shop:customer:read + admin:customer:create + sylius:admin:customer:create + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + admin:customer:update + sylius:admin:customer:update + shop:customer:show + sylius:shop:customer:show shop:customer:update + sylius:shop:customer:update + + + + admin:customer:create + sylius:admin:customer:create + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + admin:customer:update + sylius:admin:customer:update + shop:customer:show + sylius:shop:customer:show + shop:customer:update + sylius:shop:customer:update + + + + admin:customer:create + sylius:admin:customer:create + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + admin:customer:update + sylius:admin:customer:update + shop:customer:show + sylius:shop:customer:show + shop:customer:update + sylius:shop:customer:update + + + + admin:customer:create + sylius:admin:customer:create + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + admin:customer:update + sylius:admin:customer:update + shop:customer:show + sylius:shop:customer:show + shop:customer:update + sylius:shop:customer:update + + + + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CustomerGroup.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CustomerGroup.xml index 1f0b74a0bc..7665480813 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CustomerGroup.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CustomerGroup.xml @@ -17,16 +17,28 @@ > - admin:customer_group:read + admin:customer_group:index + sylius:admin:customer_group:index + admin:customer_group:show + sylius:admin:customer_group:show - admin:customer_group:read + admin:customer_group:index + sylius:admin:customer_group:index + admin:customer_group:show + sylius:admin:customer_group:show admin:customer_group:create + sylius:admin:customer_group:create - admin:customer_group:read + admin:customer_group:index + sylius:admin:customer_group:index + admin:customer_group:show + sylius:admin:customer_group:show admin:customer_group:create + sylius:admin:customer_group:create admin:customer_group:update + sylius:admin:customer_group:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/VerifyCustomerAccount.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CustomerStatistics.xml similarity index 56% rename from src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/VerifyCustomerAccount.xml rename to src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CustomerStatistics.xml index 5210d53a30..a7e1bd8dfd 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Commands/Account/VerifyCustomerAccount.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/CustomerStatistics.xml @@ -15,9 +15,14 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" > - - - shop:account_verification:update + + + admin:customer:statistics:show + sylius:admin:customer:statistics:show + + + admin:customer:statistics:show + sylius:admin:customer:statistics:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ExchangeRate.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ExchangeRate.xml index 3339e389bb..edaedfd2aa 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ExchangeRate.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ExchangeRate.xml @@ -17,24 +17,52 @@ > - admin:exchange_rate:read - shop:exchange_rate:read + admin:exchange_rate:index + sylius:admin:exchange_rate:index + admin:exchange_rate:show + sylius:admin:exchange_rate:show + shop:exchange_rate:index + sylius:shop:exchange_rate:index + shop:exchange_rate:show + sylius:shop:exchange_rate:show admin:exchange_rate:create - admin:exchange_rate:read + sylius:admin:exchange_rate:create + admin:exchange_rate:index + sylius:admin:exchange_rate:index + admin:exchange_rate:show + sylius:admin:exchange_rate:show admin:exchange_rate:update - shop:exchange_rate:read + sylius:admin:exchange_rate:update + shop:exchange_rate:index + sylius:shop:exchange_rate:index + shop:exchange_rate:show + sylius:shop:exchange_rate:show admin:exchange_rate:create - admin:exchange_rate:read - shop:exchange_rate:read + sylius:admin:exchange_rate:create + admin:exchange_rate:index + sylius:admin:exchange_rate:index + admin:exchange_rate:show + sylius:admin:exchange_rate:show + shop:exchange_rate:index + sylius:shop:exchange_rate:index + shop:exchange_rate:show + sylius:shop:exchange_rate:show admin:exchange_rate:create - admin:exchange_rate:read - shop:exchange_rate:read + sylius:admin:exchange_rate:create + admin:exchange_rate:index + sylius:admin:exchange_rate:index + admin:exchange_rate:show + sylius:admin:exchange_rate:show + shop:exchange_rate:index + sylius:shop:exchange_rate:index + shop:exchange_rate:show + sylius:shop:exchange_rate:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/GatewayConfig.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/GatewayConfig.xml new file mode 100644 index 0000000000..184a61bbf6 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/GatewayConfig.xml @@ -0,0 +1,44 @@ + + + + + + + + admin:gateway_config:show + sylius:admin:gateway_config:show + + + admin:gateway_config:show + sylius:admin:gateway_config:show + admin:payment_method:create + sylius:admin:payment_method:create + admin:payment_method:update + sylius:admin:payment_method:update + + + admin:gateway_config:show + sylius:admin:gateway_config:show + admin:payment_method:create + sylius:admin:payment_method:create + + + admin:gateway_config:show + sylius:admin:gateway_config:show + admin:payment_method:create + sylius:admin:payment_method:create + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Locale.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Locale.xml index 00d5b18b47..65544e24cb 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Locale.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Locale.xml @@ -17,16 +17,32 @@ > - admin:locale:read + admin:locale:index + sylius:admin:locale:index + admin:locale:show + sylius:admin:locale:show - admin:locale:read + admin:locale:index + sylius:admin:locale:index + admin:locale:show + sylius:admin:locale:show admin:locale:create - shop:locale:read + sylius:admin:locale:create + shop:locale:index + sylius:shop:locale:index + shop:locale:show + sylius:shop:locale:show - admin:locale:read - shop:locale:read + admin:locale:index + sylius:admin:locale:index + admin:locale:show + sylius:admin:locale:show + shop:locale:index + sylius:shop:locale:index + shop:locale:show + sylius:shop:locale:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Order.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Order.xml index 4cba9b5ea0..5eb7082067 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Order.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Order.xml @@ -17,143 +17,319 @@ > - admin:order:read - shop:cart:read - shop:order:account:read - shop:order:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + shop:order:index + sylius:shop:order:index + shop:order:show + sylius:shop:order:show - admin:order:read - shop:cart:read - shop:order:account:read - shop:order:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + shop:order:index + sylius:shop:order:index + shop:order:show + sylius:shop:order:show - admin:order:read - shop:cart:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show - admin:order:read - shop:order:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:order:index + sylius:shop:order:index + shop:order:show + sylius:shop:order:show - admin:order:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show - admin:order:read - shop:cart:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - shop:order:account:read - shop:cart:read - admin:order:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:order:account:show + sylius:shop:order:account:show + shop:cart:show + sylius:shop:cart:show - shop:order:account:read - shop:cart:read - admin:order:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:order:account:show + sylius:shop:order:account:show + shop:cart:show + sylius:shop:cart:show - admin:order:read - shop:order:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:order:index + sylius:shop:order:index + shop:order:show + sylius:shop:order:show - admin:order:read - shop:cart:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show - admin:order:read - shop:cart:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show - admin:order:read - shop:cart:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:order:read - shop:cart:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + + + + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:order:read - shop:cart:read - shop:order:account:read - shop:order:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + shop:order:index + sylius:shop:order:index + shop:order:show + sylius:shop:order:show - admin:order:read - shop:cart:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:order:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show - admin:order:read - shop:cart:read - shop:order:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:index + sylius:shop:order:index + shop:order:show + sylius:shop:order:show + shop:order:account:show + sylius:shop:order:account:show + + + + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:index + sylius:shop:order:index + shop:order:show + sylius:shop:order:show + shop:order:account:show + sylius:shop:order:account:show - admin:order:read - shop:cart:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + + + + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:order:read - shop:cart:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:order:read - shop:cart:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + + + + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show admin:cart:update - admin:order:read - shop:cart:read - shop:order:account:read + sylius:admin:cart:update + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:order:read - shop:cart:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:order:read - shop:cart:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show - admin:order:read - shop:cart:read - shop:order:account:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/OrderItem.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/OrderItem.xml index 9c2eaead0d..040eaabf89 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/OrderItem.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/OrderItem.xml @@ -17,88 +17,168 @@ > - admin:order:read - admin:order_item:read - shop:cart:read - shop:order:account:read - shop:order_item:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:order_item:show + sylius:admin:order_item:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + shop:order_item:show + sylius:shop:order_item:show - admin:order_item:read - shop:order_item:read + admin:order_item:show + sylius:admin:order_item:show + shop:order_item:show + sylius:shop:order_item:show - admin:order:read - admin:order_item:read - shop:cart:read - shop:order:account:read - shop:order_item:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:order_item:show + sylius:admin:order_item:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + shop:order_item:show + sylius:shop:order_item:show - admin:order:read - admin:order_item:read - shop:cart:read - shop:order:account:read - shop:order_item:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:order_item:show + sylius:admin:order_item:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + shop:order_item:show + sylius:shop:order_item:show - admin:order:read - admin:order_item:read - shop:order_item:read - shop:cart:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:order_item:show + sylius:admin:order_item:show + shop:order_item:show + sylius:shop:order_item:show + shop:cart:show + sylius:shop:cart:show - admin:order:read - admin:order_item:read - shop:order_item:read - shop:cart:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:order_item:show + sylius:admin:order_item:show + shop:order_item:show + sylius:shop:order_item:show + shop:cart:show + sylius:shop:cart:show - shop:order_item:read - shop:cart:read + shop:order_item:show + sylius:shop:order_item:show + shop:cart:show + sylius:shop:cart:show - admin:order_item:read - shop:order_item:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:order_item:show + sylius:admin:order_item:show + shop:order_item:show + sylius:shop:order_item:show - admin:order_item:read - shop:order_item:read + admin:order_item:show + sylius:admin:order_item:show + shop:order_item:show + sylius:shop:order_item:show - admin:order_item:read - shop:order_item:read + admin:order_item:show + sylius:admin:order_item:show + shop:order_item:show + sylius:shop:order_item:show - admin:order_item:read - shop:order_item:read + admin:order_item:show + sylius:admin:order_item:show + shop:order_item:show + sylius:shop:order_item:show - admin:order_item:read - shop:order_item:read + admin:order_item:show + sylius:admin:order_item:show + shop:order_item:show + sylius:shop:order_item:show - admin:order_item:read - shop:order_item:read + admin:order_item:show + sylius:admin:order_item:show + shop:order_item:show + sylius:shop:order_item:show - admin:order:read - admin:order_item:read - shop:cart:read - shop:order:account:read - shop:order_item:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:order_item:show + sylius:admin:order_item:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + shop:order_item:show + sylius:shop:order_item:show - admin:order:read - admin:order_item:read - shop:cart:read - shop:order_item:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:order_item:show + sylius:admin:order_item:show + shop:cart:show + sylius:shop:cart:show + shop:order_item:show + sylius:shop:order_item:show - admin:order:read - admin:order_item:read - shop:order_item:read - shop:cart:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:order_item:show + sylius:admin:order_item:show + shop:order_item:show + sylius:shop:order_item:show + shop:cart:show + sylius:shop:cart:show + + + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:order_item:show + sylius:admin:order_item:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/OrderItemUnit.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/OrderItemUnit.xml index 5b34acbd72..ccf5eefc55 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/OrderItemUnit.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/OrderItemUnit.xml @@ -17,12 +17,16 @@ > - admin:order_item_unit:read - shop:order_item_unit:read + admin:order_item_unit:show + sylius:admin:order_item_unit:show + shop:order_item_unit:show + sylius:shop:order_item_unit:show - admin:order_item_unit:read - shop:order_item_unit:read + admin:order_item_unit:show + sylius:admin:order_item_unit:show + shop:order_item_unit:show + sylius:shop:order_item_unit:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Payment.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Payment.xml index 58d37abd5a..f69adbd1c3 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Payment.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Payment.xml @@ -17,45 +17,90 @@ > - admin:order:read - admin:payment:read - shop:cart:read - shop:payment:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + shop:cart:show + sylius:shop:cart:show + shop:payment:show + sylius:shop:payment:show - admin:order:read - admin:payment:read - shop:cart:read - shop:order:account:read - shop:payment:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + shop:payment:show + sylius:shop:payment:show - admin:payment:read - shop:payment:read + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + shop:payment:show + sylius:shop:payment:show - admin:payment:read - shop:payment:read + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + shop:payment:show + sylius:shop:payment:show - admin:payment:read - shop:payment:read + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + shop:payment:show + sylius:shop:payment:show - admin:payment:read - shop:payment:read + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + shop:payment:show + sylius:shop:payment:show - admin:payment:read - shop:payment:read + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + shop:payment:show + sylius:shop:payment:show - admin:payment:read - shop:payment:read + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + shop:payment:show + sylius:shop:payment:show - admin:payment:read - shop:payment:read + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + shop:payment:show + sylius:shop:payment:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PaymentMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PaymentMethod.xml index 495bc2db99..0781097cdf 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PaymentMethod.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PaymentMethod.xml @@ -17,28 +17,115 @@ > - admin:payment_method:read - shop:payment_method:read + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + sylius:admin:payment_method:show + shop:payment_method:index + sylius:shop:payment_method:index + shop:payment_method:show + sylius:shop:payment_method:show + + + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + admin:payment_method:create + sylius:admin:payment_method:create + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + sylius:admin:payment_method:show + admin:payment_method:update + sylius:admin:payment_method:update + + - admin:payment:read - admin:payment_method:read - shop:order:account:read - shop:payment:read - shop:payment_method:read + shop:payment:show + sylius:shop:payment:show + shop:payment_method:index + sylius:shop:payment_method:index + shop:payment_method:show + sylius:shop:payment_method:show + + + admin:payment_method:create + sylius:admin:payment_method:create + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + sylius:admin:payment_method:show + admin:payment_method:update + sylius:admin:payment_method:update + + - admin:payment_method:read - shop:payment_method:read + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + sylius:admin:payment_method:show + admin:payment_method:create + sylius:admin:payment_method:create + shop:payment_method:index + sylius:shop:payment_method:index + shop:payment_method:show + sylius:shop:payment_method:show + - shop:payment_method:read + shop:payment_method:index + sylius:shop:payment_method:index + shop:payment_method:show + sylius:shop:payment_method:show + - shop:payment_method:read + shop:payment_method:index + sylius:shop:payment_method:index + shop:payment_method:show + sylius:shop:payment_method:show + + + admin:payment_method:create + sylius:admin:payment_method:create + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + sylius:admin:payment_method:show + admin:payment_method:update + sylius:admin:payment_method:update + + - shop:payment_method:read + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + sylius:admin:payment_method:show + admin:payment_method:create + sylius:admin:payment_method:create + admin:payment_method:update + sylius:admin:payment_method:update + shop:payment_method:index + sylius:shop:payment_method:index + shop:payment_method:show + sylius:shop:payment_method:show + + + + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + admin:gateway_config:show + sylius:admin:gateway_config:show + sylius:admin:payment_method:show + admin:payment_method:create + sylius:admin:payment_method:create + admin:payment_method:update + sylius:admin:payment_method:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PaymentMethodTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PaymentMethodTranslation.xml new file mode 100644 index 0000000000..4b161d98d6 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PaymentMethodTranslation.xml @@ -0,0 +1,70 @@ + + + + + + + + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + sylius:admin:payment_method:show + + + + admin:payment:index + sylius:admin:payment:index + admin:payment:show + sylius:admin:payment:show + admin:payment_method:create + sylius:admin:payment_method:create + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + sylius:admin:payment_method:show + admin:payment_method:update + sylius:admin:payment_method:update + + + + admin:payment_method:create + sylius:admin:payment_method:create + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + sylius:admin:payment_method:show + admin:payment_method:update + sylius:admin:payment_method:update + + + + admin:payment_method:create + sylius:admin:payment_method:create + admin:payment_method:index + sylius:admin:payment_method:index + admin:payment_method:show + sylius:admin:payment_method:show + admin:payment_method:update + sylius:admin:payment_method:update + + + + admin:payment_method:create + sylius:admin:payment_method:create + admin:payment_method:update + sylius:admin:payment_method:update + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PerChannelCustomerStatistics.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PerChannelCustomerStatistics.xml new file mode 100644 index 0000000000..22e24346a9 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PerChannelCustomerStatistics.xml @@ -0,0 +1,36 @@ + + + + + + + + admin:customer:statistics:show + sylius:admin:customer:statistics:show + + + admin:customer:statistics:show + sylius:admin:customer:statistics:show + + + admin:customer:statistics:show + sylius:admin:customer:statistics:show + + + admin:customer:statistics:show + sylius:admin:customer:statistics:show + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Product.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Product.xml index b737a2aaf8..444f9ca80d 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Product.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Product.xml @@ -17,76 +17,215 @@ > - admin:product:read - shop:product:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show admin:product:create - admin:product:read - shop:product:read + sylius:admin:product:create + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show + + + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + + + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update - shop:product:read + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show - shop:product:read + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show - shop:product:read + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show - shop:product:read - - - admin:product:read - shop:product:read - - - admin:product:read - shop:product:read + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show admin:product:create - admin:product:read + sylius:admin:product:create admin:product:update + sylius:admin:product:update + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show admin:product:create - admin:product:read + sylius:admin:product:create admin:product:update - shop:product:read + sylius:admin:product:update + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show - admin:product:create - admin:product:read - admin:product:update - shop:product:read - - - admin:product:read - shop:product:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show - admin:product:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show admin:product:create + sylius:admin:product:create admin:product:update - shop:product:read - - - admin:product:read - admin:product:create - admin:product:update - shop:product:read + sylius:admin:product:update + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show - admin:product:read - shop:product:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show + + + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show + + + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + + + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show + + true + - shop:product:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show - shop:product:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show + + + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update + + + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update + + + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show + + + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociation.xml index 90affc1d3f..b45595b3e2 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociation.xml @@ -17,13 +17,38 @@ > - shop:product_association:read + shop:product_association:show + sylius:shop:product_association:show - shop:product_association:read + admin:product_association:index + sylius:admin:product_association:index + admin:product_association:show + sylius:admin:product_association:show + admin:product_association:create + sylius:admin:product_association:create + shop:product_association:show + sylius:shop:product_association:show + + + admin:product_association:index + sylius:admin:product_association:index + admin:product_association:show + sylius:admin:product_association:show + admin:product_association:create + sylius:admin:product_association:create - shop:product_association:read + admin:product_association:index + sylius:admin:product_association:index + admin:product_association:show + sylius:admin:product_association:show + admin:product_association:create + sylius:admin:product_association:create + admin:product_association:update + sylius:admin:product_association:update + shop:product_association:show + sylius:shop:product_association:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociationType.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociationType.xml index 304c3f1a50..b554bf5188 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociationType.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociationType.xml @@ -17,30 +17,52 @@ > - admin:product_association_type:read - shop:product_association_type:read + admin:product_association_type:index + sylius:admin:product_association_type:index + admin:product_association_type:show + sylius:admin:product_association_type:show + shop:product_association_type:show + sylius:shop:product_association_type:show - admin:product_association_type:read + admin:product_association_type:index + sylius:admin:product_association_type:index + admin:product_association_type:show + sylius:admin:product_association_type:show admin:product_association_type:create - shop:product_association_type:read + sylius:admin:product_association_type:create + shop:product_association_type:show + sylius:shop:product_association_type:show - admin:product_association_type:read - shop:product_association_type:read + admin:product_association_type:index + sylius:admin:product_association_type:index + admin:product_association_type:show + sylius:admin:product_association_type:show + shop:product_association_type:show + sylius:shop:product_association_type:show - admin:product_association_type:read - shop:product_association_type:read + admin:product_association_type:index + sylius:admin:product_association_type:index + admin:product_association_type:show + sylius:admin:product_association_type:show - admin:product_association_type:read - shop:product_association_type:read + admin:product_association_type:index + sylius:admin:product_association_type:index + admin:product_association_type:show + sylius:admin:product_association_type:show - admin:product_association_type:read + admin:product_association_type:index + sylius:admin:product_association_type:index + admin:product_association_type:show + sylius:admin:product_association_type:show admin:product_association_type:create + sylius:admin:product_association_type:create admin:product_association_type:update + sylius:admin:product_association_type:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociationTypeTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociationTypeTranslation.xml index c5240a2ce6..6b7869b9c2 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociationTypeTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAssociationTypeTranslation.xml @@ -17,16 +17,26 @@ > - admin:product_association_type:read + admin:product_association_type:index + sylius:admin:product_association_type:index + admin:product_association_type:show + sylius:admin:product_association_type:show - admin:product_association_type:read + admin:product_association_type:index + sylius:admin:product_association_type:index + admin:product_association_type:show + sylius:admin:product_association_type:show admin:product_association_type:create + sylius:admin:product_association_type:create admin:product_association_type:update + sylius:admin:product_association_type:update admin:product_association_type:create + sylius:admin:product_association_type:create admin:product_association_type:update + sylius:admin:product_association_type:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttribute.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttribute.xml index ba5ac79b3a..9ae2af34b4 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttribute.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttribute.xml @@ -17,19 +17,78 @@ > - shop:product_attribute:read + shop:product_attribute:show + sylius:shop:product_attribute:show - shop:product_attribute:read + shop:product_attribute:show + sylius:shop:product_attribute:show - shop:product_attribute:read + admin:product_attribute:index + sylius:admin:product_attribute:index + admin:product_attribute:show + sylius:admin:product_attribute:show + admin:product_attribute:create + sylius:admin:product_attribute:create + shop:product_attribute:show + sylius:shop:product_attribute:show - shop:product_attribute:read + admin:product_attribute:index + sylius:admin:product_attribute:index + admin:product_attribute:show + sylius:admin:product_attribute:show + admin:product_attribute:create + sylius:admin:product_attribute:create + shop:product_attribute:show + sylius:shop:product_attribute:show + + + admin:product_attribute:index + sylius:admin:product_attribute:index + admin:product_attribute:show + sylius:admin:product_attribute:show + + + admin:product_attribute:index + sylius:admin:product_attribute:index + admin:product_attribute:show + sylius:admin:product_attribute:show + admin:product_attribute:create + sylius:admin:product_attribute:create + admin:product_attribute:update + sylius:admin:product_attribute:update + + + admin:product_attribute:index + sylius:admin:product_attribute:index + admin:product_attribute:show + sylius:admin:product_attribute:show + admin:product_attribute:create + sylius:admin:product_attribute:create + admin:product_attribute:update + sylius:admin:product_attribute:update - shop:product_attribute:read + admin:product_attribute:index + sylius:admin:product_attribute:index + admin:product_attribute:show + sylius:admin:product_attribute:show + shop:product_attribute:show + sylius:shop:product_attribute:show + admin:product_attribute:update + sylius:admin:product_attribute:update + + + admin:product_attribute:index + sylius:admin:product_attribute:index + admin:product_attribute:show + sylius:admin:product_attribute:show + admin:product_attribute:create + sylius:admin:product_attribute:create + admin:product_attribute:update + sylius:admin:product_attribute:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttributeTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttributeTranslation.xml new file mode 100644 index 0000000000..ae7bcff1f6 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttributeTranslation.xml @@ -0,0 +1,46 @@ + + + + + + + + admin:product_attribute:index + sylius:admin:product_attribute:index + admin:product_attribute:show + sylius:admin:product_attribute:show + shop:product_attribute:show + sylius:shop:product_attribute:show + + + admin:product_attribute:create + sylius:admin:product_attribute:create + admin:product_attribute:update + sylius:admin:product_attribute:update + + + admin:product_attribute:index + sylius:admin:product_attribute:index + admin:product_attribute:show + sylius:admin:product_attribute:show + admin:product_attribute:create + sylius:admin:product_attribute:create + admin:product_attribute:update + sylius:admin:product_attribute:update + shop:product_attribute:show + sylius:shop:product_attribute:show + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttributeValue.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttributeValue.xml index a9f85b4401..c5ddee086b 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttributeValue.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductAttributeValue.xml @@ -17,25 +17,74 @@ > - shop:product_attribute_value:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product_attribute_value:show + sylius:shop:product_attribute_value:show + admin:product_attribute_value:show + sylius:admin:product_attribute_value:show - shop:product_attribute_value:read - - - shop:product_attribute_value:read - - - shop:product_attribute_value:read - - - shop:product_attribute_value:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update + shop:product_attribute_value:show + sylius:shop:product_attribute_value:show + admin:product_attribute_value:show + sylius:admin:product_attribute_value:show - shop:product_attribute_value:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update + shop:product_attribute_value:show + sylius:shop:product_attribute_value:show + admin:product_attribute_value:show + sylius:admin:product_attribute_value:show - shop:product_attribute_value:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update + shop:product_attribute_value:show + sylius:shop:product_attribute_value:show + admin:product_attribute_value:show + sylius:admin:product_attribute_value:show + + + shop:product_attribute_value:show + sylius:shop:product_attribute_value:show + admin:product_attribute_value:show + sylius:admin:product_attribute_value:show + + + shop:product_attribute_value:show + sylius:shop:product_attribute_value:show + admin:product_attribute_value:show + sylius:admin:product_attribute_value:show + + + shop:product_attribute_value:show + sylius:shop:product_attribute_value:show + admin:product_attribute_value:show + sylius:admin:product_attribute_value:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductImage.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductImage.xml index cdad84aad2..1ce6fb946b 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductImage.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductImage.xml @@ -1,27 +1,88 @@ + + - admin:product_image:read - shop:product_image:read - admin:product:read - shop:product:read + admin:product_image:index + sylius:admin:product_image:index + admin:product_image:show + sylius:admin:product_image:show + shop:product_image:show + sylius:shop:product_image:show + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show - admin:product_image:read - shop:product_image:read - admin:product:read - shop:product:read + admin:product_image:index + sylius:admin:product_image:index + admin:product_image:show + sylius:admin:product_image:show + shop:product_image:show + sylius:shop:product_image:show + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show + + + admin:product_image:index + sylius:admin:product_image:index + admin:product_image:show + sylius:admin:product_image:show - admin:product_image:read - shop:product_image:read - admin:product:read - shop:product:read + admin:product_image:index + sylius:admin:product_image:index + admin:product_image:show + sylius:admin:product_image:show + admin:product_image:update + sylius:admin:product_image:update + shop:product_image:show + sylius:shop:product_image:show + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + shop:product:index + sylius:shop:product:index + shop:product:show + sylius:shop:product:show + + + admin:product_image:index + sylius:admin:product_image:index + admin:product_image:show + sylius:admin:product_image:show + admin:product_image:update + sylius:admin:product_image:update + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOption.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOption.xml index e797fc8fd3..4937683ec5 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOption.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOption.xml @@ -17,36 +17,74 @@ > - admin:product_option:read - shop:product_option:read + admin:product_option:index + sylius:admin:product_option:index + admin:product_option:show + sylius:admin:product_option:show + shop:product_option:show + sylius:shop:product_option:show - admin:product_option:read + admin:product_option:index + sylius:admin:product_option:index + admin:product_option:show + sylius:admin:product_option:show admin:product_option:create - shop:product_option:read + sylius:admin:product_option:create + shop:product_option:show + sylius:shop:product_option:show - admin:product_option:read - shop:product_option:read + admin:product_option:index + sylius:admin:product_option:index + admin:product_option:show + sylius:admin:product_option:show + shop:product_option:show + sylius:shop:product_option:show - admin:product_option:read - shop:product_option:read + admin:product_option:index + sylius:admin:product_option:index + admin:product_option:show + sylius:admin:product_option:show - admin:product_option:read - shop:product_option:read + admin:product_option:index + sylius:admin:product_option:index + admin:product_option:show + sylius:admin:product_option:show - admin:product_option:read + admin:product_option:index + sylius:admin:product_option:index + admin:product_option:show + sylius:admin:product_option:show admin:product_option:create + sylius:admin:product_option:create admin:product_option:update - shop:product_option:read + sylius:admin:product_option:update + shop:product_option:show + sylius:shop:product_option:show - admin:product_option:read + admin:product_option:index + sylius:admin:product_option:index + admin:product_option:show + sylius:admin:product_option:show admin:product_option:create + sylius:admin:product_option:create admin:product_option:update + sylius:admin:product_option:update + + + admin:product_option:index + sylius:admin:product_option:index + admin:product_option:show + sylius:admin:product_option:show + admin:product_option:create + sylius:admin:product_option:create + admin:product_option:update + sylius:admin:product_option:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionTranslation.xml index 99aeda3ad7..19d61e0915 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionTranslation.xml @@ -17,16 +17,26 @@ > - admin:product_option:read + admin:product_option:index + sylius:admin:product_option:index + admin:product_option:show + sylius:admin:product_option:show - admin:product_option:read + admin:product_option:index + sylius:admin:product_option:index + admin:product_option:show + sylius:admin:product_option:show admin:product_option:create + sylius:admin:product_option:create admin:product_option:update + sylius:admin:product_option:update admin:product_option:create + sylius:admin:product_option:create admin:product_option:update + sylius:admin:product_option:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionValue.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionValue.xml index 6f5225a0cc..db79d9c9c6 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionValue.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionValue.xml @@ -17,29 +17,44 @@ > - admin:product_option_value:read - shop:product_option_value:read + admin:product_option_value:show + sylius:admin:product_option_value:show + shop:product_option_value:show + sylius:shop:product_option_value:show - admin:product_option_value:read - shop:product_option_value:read + admin:product_option_value:show + sylius:admin:product_option_value:show + shop:product_option_value:show + sylius:shop:product_option_value:show - admin:product_option_value:read - shop:product_option_value:read + admin:product_option_value:show + sylius:admin:product_option_value:show + shop:product_option_value:show + sylius:shop:product_option_value:show admin:product_option:create + sylius:admin:product_option:create admin:product_option:update - admin:product_option_value:read + sylius:admin:product_option:update + admin:product_option_value:show + sylius:admin:product_option_value:show admin:product_option_value:write - shop:product_option_value:read + sylius:admin:product_option_value:write + shop:product_option_value:show + sylius:shop:product_option_value:show admin:product_option:create + sylius:admin:product_option:create admin:product_option:update - admin:product_option_value:read + sylius:admin:product_option:update + admin:product_option_value:show + sylius:admin:product_option_value:show admin:product_option_value:write + sylius:admin:product_option_value:write diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionValueTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionValueTranslation.xml index 2bd5ce37e0..08513bca29 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionValueTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductOptionValueTranslation.xml @@ -17,18 +17,26 @@ > - admin:product_option_value:read + admin:product_option_value:show + sylius:admin:product_option_value:show admin:product_option:create + sylius:admin:product_option:create admin:product_option:update - admin:product_option_value:read + sylius:admin:product_option:update + admin:product_option_value:show + sylius:admin:product_option_value:show admin:product_option_value:write + sylius:admin:product_option_value:write admin:product_option:create + sylius:admin:product_option:create admin:product_option:update + sylius:admin:product_option:update admin:product_option_value:write + sylius:admin:product_option_value:write diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductReview.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductReview.xml index 3d820e8b93..6901d87b9b 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductReview.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductReview.xml @@ -17,44 +17,100 @@ > - admin:product_review:read - shop:product_review:read + admin:product_review:index + sylius:admin:product_review:index + admin:product_review:show + sylius:admin:product_review:show + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show - admin:product_review:read - shop:product_review:read - + admin:product_review:index + sylius:admin:product_review:index + admin:product_review:show + sylius:admin:product_review:show + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show + - admin:product_review:read - shop:product_review:read - - - admin:product_review:read - admin:product_review:update - shop:product_review:read - - - admin:product_review:read - admin:product_review:update - shop:product_review:read - - - admin:product_review:read - admin:product_review:update - shop:product_review:read - shop:product_review:create - - - admin:product_review:read - shop:product_review:read - - - admin:product_review:read - shop:product_review:read - - - admin:product_review:read - shop:product_review:read - + admin:product_review:index + sylius:admin:product_review:index + admin:product_review:show + sylius:admin:product_review:show + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show + + + admin:product_review:index + sylius:admin:product_review:index + admin:product_review:show + sylius:admin:product_review:show + admin:product_review:update + sylius:admin:product_review:update + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show + + + admin:product_review:index + sylius:admin:product_review:index + admin:product_review:show + sylius:admin:product_review:show + admin:product_review:update + sylius:admin:product_review:update + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show + + + admin:product_review:index + sylius:admin:product_review:index + admin:product_review:show + sylius:admin:product_review:show + admin:product_review:update + sylius:admin:product_review:update + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show + + + admin:product_review:index + sylius:admin:product_review:index + admin:product_review:show + sylius:admin:product_review:show + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show + + + admin:product_review:index + sylius:admin:product_review:index + admin:product_review:show + sylius:admin:product_review:show + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show + + + admin:product_review:index + sylius:admin:product_review:index + admin:product_review:show + sylius:admin:product_review:show + shop:product_review:index + sylius:shop:product_review:index + shop:product_review:show + sylius:shop:product_review:show + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductTaxon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductTaxon.xml index 3aad737e95..273717f1d8 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductTaxon.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductTaxon.xml @@ -1,17 +1,60 @@ + + - admin:product_taxon:read - shop:product_taxon:read + admin:product_taxon:index + sylius:admin:product_taxon:index + admin:product_taxon:show + sylius:admin:product_taxon:show + shop:product_taxon:show + sylius:shop:product_taxon:show - admin:product_taxon:read - shop:product_taxon:read + admin:product_taxon:index + sylius:admin:product_taxon:index + admin:product_taxon:show + sylius:admin:product_taxon:show + admin:product_taxon:create + sylius:admin:product_taxon:create + shop:product_taxon:show + sylius:shop:product_taxon:show + + + admin:product_taxon:index + sylius:admin:product_taxon:index + admin:product_taxon:show + sylius:admin:product_taxon:show + admin:product_taxon:create + sylius:admin:product_taxon:create + shop:product_taxon:show + sylius:shop:product_taxon:show + + + admin:product_taxon:index + sylius:admin:product_taxon:index + admin:product_taxon:show + sylius:admin:product_taxon:show + admin:product_taxon:update + sylius:admin:product_taxon:update + admin:product_taxon:create + sylius:admin:product_taxon:create + shop:product_taxon:show + sylius:shop:product_taxon:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductTranslation.xml index ebb09e77f4..a8f563e792 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductTranslation.xml @@ -1,46 +1,92 @@ + + - admin:product:read - shop:product:read - - - admin:product:create - admin:product:read - admin:product:update - shop:product:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update + + + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show admin:product:create - admin:product:read + sylius:admin:product:create admin:product:update - shop:product:read + sylius:admin:product:update - shop:product:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update - shop:product:read + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show + admin:product:create + sylius:admin:product:create + admin:product:update + sylius:admin:product:update + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show admin:product:create - admin:product:read + sylius:admin:product:create admin:product:update - shop:product:read + sylius:admin:product:update + admin:product:index + sylius:admin:product:index + admin:product:show + sylius:admin:product:show admin:product:create - admin:product:read + sylius:admin:product:create admin:product:update - shop:product:read + sylius:admin:product:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml index 4fabd1fb50..140b1bb326 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariant.xml @@ -16,34 +16,201 @@ xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" > - - admin:product_variant:read - shop:product_variant:read - - admin:product_variant:read + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show admin:product_variant:create - shop:product_variant:read + sylius:admin:product_variant:create + shop:product_variant:index + sylius:shop:product_variant:index + shop:product_variant:show + sylius:shop:product_variant:show - admin:product_variant:read + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show admin:product_variant:create - shop:product_variant:read + sylius:admin:product_variant:create + shop:product_variant:index + sylius:shop:product_variant:index + shop:product_variant:show + sylius:shop:product_variant:show - admin:product_variant:read + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update - admin:product_variant:read - shop:product_variant:read + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + shop:product_variant:index + sylius:shop:product_variant:index + shop:product_variant:show + sylius:shop:product_variant:show + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show admin:product_variant:create - admin:product_variant:read + sylius:admin:product_variant:create admin:product_variant:update + sylius:admin:product_variant:update - shop:product_variant:read + shop:product_variant:index + sylius:shop:product_variant:index + shop:product_variant:show + sylius:shop:product_variant:show + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + + + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariantTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariantTranslation.xml index f4ccbc3351..8b50080916 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariantTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ProductVariantTranslation.xml @@ -1,27 +1,50 @@ + + - shop:order:account:read - admin:order:read - admin:product_variant:read - shop:product_variant:read + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show - shop:order:account:read - admin:order:read - admin:product_variant:read - shop:product_variant:read + admin:product_variant:index + sylius:admin:product_variant:index + admin:product_variant:show + sylius:admin:product_variant:show + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show - shop:order:account:read - admin:order:read - admin:product_variant:read - shop:product_variant:read + admin:product_variant:create + sylius:admin:product_variant:create + admin:product_variant:update + sylius:admin:product_variant:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Promotion.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Promotion.xml index 40e90b998e..cb42f7fbc3 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Promotion.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Promotion.xml @@ -13,66 +13,195 @@ > - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show admin:promotion:create + sylius:admin:promotion:create - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update + + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update - admin:promotion:read + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update + + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update + + + admin:promotion:read + sylius:admin:promotion:read diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionAction.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionAction.xml new file mode 100644 index 0000000000..4545d26e79 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionAction.xml @@ -0,0 +1,42 @@ + + + + + + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionCoupon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionCoupon.xml new file mode 100644 index 0000000000..6721aaceec --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionCoupon.xml @@ -0,0 +1,102 @@ + + + + + + + + admin:promotion_coupon:index + sylius:admin:promotion_coupon:index + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + + + + admin:promotion_coupon:index + sylius:admin:promotion_coupon:index + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + + + + admin:promotion_coupon:index + sylius:admin:promotion_coupon:index + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + admin:promotion_coupon:create + sylius:admin:promotion_coupon:create + + + + admin:promotion_coupon:index + sylius:admin:promotion_coupon:index + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + admin:promotion_coupon:create + sylius:admin:promotion_coupon:create + admin:promotion_coupon:update + sylius:admin:promotion_coupon:update + + + + admin:promotion_coupon:index + sylius:admin:promotion_coupon:index + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + admin:promotion_coupon:create + sylius:admin:promotion_coupon:create + admin:promotion_coupon:update + sylius:admin:promotion_coupon:update + + + + admin:promotion_coupon:index + sylius:admin:promotion_coupon:index + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + + + + admin:promotion_coupon:index + sylius:admin:promotion_coupon:index + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + admin:promotion_coupon:create + sylius:admin:promotion_coupon:create + admin:promotion_coupon:update + sylius:admin:promotion_coupon:update + + + + admin:promotion_coupon:index + sylius:admin:promotion_coupon:index + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + admin:promotion_coupon:create + sylius:admin:promotion_coupon:create + + + + admin:promotion_coupon:index + sylius:admin:promotion_coupon:index + admin:promotion_coupon:show + sylius:admin:promotion_coupon:show + admin:promotion_coupon:create + sylius:admin:promotion_coupon:create + admin:promotion_coupon:update + sylius:admin:promotion_coupon:update + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionRule.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionRule.xml new file mode 100644 index 0000000000..e71b6fba58 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionRule.xml @@ -0,0 +1,42 @@ + + + + + + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionTranslation.xml new file mode 100644 index 0000000000..0e01f5ece9 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/PromotionTranslation.xml @@ -0,0 +1,44 @@ + + + + + + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + + + + admin:promotion:index + sylius:admin:promotion:index + admin:promotion:show + sylius:admin:promotion:show + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update + + + + admin:promotion:create + sylius:admin:promotion:create + admin:promotion:update + sylius:admin:promotion:update + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Province.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Province.xml index 676a64a977..979890d21b 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Province.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Province.xml @@ -16,33 +16,45 @@ xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" > - - admin:province:read + + admin:province:show + sylius:admin:province:show + admin:country:create + sylius:admin:country:create + admin:country:update + sylius:admin:country:update + shop:province:show + sylius:shop:province:show + shop:country:index + sylius:shop:country:index + shop:country:show + sylius:shop:country:show + + + admin:province:show + sylius:admin:province:show + admin:province:update + sylius:admin:province:update + admin:country:create + sylius:admin:country:create + admin:country:update + sylius:admin:country:update + shop:province:show + sylius:shop:province:show + shop:country:index + sylius:shop:country:index + shop:country:show + sylius:shop:country:show + + + admin:country:create + sylius:admin:country:create + admin:country:update + sylius:admin:country:update + admin:province:show + sylius:admin:province:show + admin:province:update + sylius:admin:province:update - - admin:province:read - - - admin:province:read - - - admin:country:create - admin:country:update - shop:country:read - admin:province:read - - - admin:country:create - admin:country:update - shop:country:read - admin:province:read - admin:province:update - - - admin:country:create - admin:country:update - admin:province:read - admin:province:update - diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Shipment.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Shipment.xml index b082b51a94..7451e3ebc9 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Shipment.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Shipment.xml @@ -17,53 +17,98 @@ > - admin:order:read - admin:shipment:read - shop:cart:read - shop:shipment:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:shipment:index + sylius:admin:shipment:index + admin:shipment:show + sylius:admin:shipment:show + shop:cart:show + sylius:shop:cart:show + shop:shipment:show + sylius:shop:shipment:show - admin:shipment:read - shop:shipment:read + admin:shipment:index + sylius:admin:shipment:index + admin:shipment:show + sylius:admin:shipment:show + shop:shipment:show + sylius:shop:shipment:show - admin:shipment:read - shop:shipment:read + admin:shipment:index + sylius:admin:shipment:index + admin:shipment:show + sylius:admin:shipment:show + shop:shipment:show + sylius:shop:shipment:show - admin:shipment:read - shop:shipment:read + admin:shipment:index + sylius:admin:shipment:index + admin:shipment:show + sylius:admin:shipment:show + shop:shipment:show + sylius:shop:shipment:show - admin:order:read - admin:shipment:read - shop:cart:read - shop:order:account:read - shop:shipment:read + admin:order:index + sylius:admin:order:index + admin:order:show + sylius:admin:order:show + admin:shipment:index + sylius:admin:shipment:index + admin:shipment:show + sylius:admin:shipment:show + shop:cart:show + sylius:shop:cart:show + shop:order:account:show + sylius:shop:order:account:show + shop:shipment:show + sylius:shop:shipment:show - admin:shipment:read - shop:shipment:read + admin:shipment:index + sylius:admin:shipment:index + admin:shipment:show + sylius:admin:shipment:show + shop:shipment:show + sylius:shop:shipment:show - admin:shipment:read - shop:shipment:read + admin:shipment:index + sylius:admin:shipment:index + admin:shipment:show + sylius:admin:shipment:show + shop:shipment:show + sylius:shop:shipment:show - admin:shipment:read - shop:shipment:read + admin:shipment:index + sylius:admin:shipment:index + admin:shipment:show + sylius:admin:shipment:show + shop:shipment:show + sylius:shop:shipment:show - admin:shipment:read - shop:shipment:read + admin:shipment:index + sylius:admin:shipment:index + admin:shipment:show + sylius:admin:shipment:show + shop:shipment:show + sylius:shop:shipment:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingCategory.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingCategory.xml index 4fd6f20590..410ec5abd2 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingCategory.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingCategory.xml @@ -17,27 +17,50 @@ > - admin:shipping_category:read + admin:shipping_category:index + sylius:admin:shipping_category:index + admin:shipping_category:show + sylius:admin:shipping_category:show - admin:shipping_category:read + admin:shipping_category:index + sylius:admin:shipping_category:index + admin:shipping_category:show + sylius:admin:shipping_category:show admin:shipping_category:create + sylius:admin:shipping_category:create - admin:shipping_category:read + admin:shipping_category:index + sylius:admin:shipping_category:index + admin:shipping_category:show + sylius:admin:shipping_category:show admin:shipping_category:update + sylius:admin:shipping_category:update admin:shipping_category:create + sylius:admin:shipping_category:create - admin:shipping_category:read + admin:shipping_category:index + sylius:admin:shipping_category:index + admin:shipping_category:show + sylius:admin:shipping_category:show admin:shipping_category:update + sylius:admin:shipping_category:update admin:shipping_category:create + sylius:admin:shipping_category:create - admin:shipping_category:read + admin:shipping_category:index + sylius:admin:shipping_category:index + admin:shipping_category:show + sylius:admin:shipping_category:show - admin:shipping_category:read + admin:shipping_category:index + sylius:admin:shipping_category:index + admin:shipping_category:show + sylius:admin:shipping_category:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethod.xml index 0c27556af0..e32a7c96fe 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethod.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethod.xml @@ -17,78 +17,154 @@ > - admin:shipping_method:read - shop:shipping_method:read + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show + shop:shipping_method:index + sylius:shop:shipping_method:index + shop:shipping_method:show + sylius:shop:shipping_method:show admin:shipping_method:create - admin:shipping_method:read + sylius:admin:shipping_method:create + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show admin:shipping_method:update + sylius:admin:shipping_method:update admin:shipping_method:create - admin:shipping_method:read - shop:shipping_method:read + sylius:admin:shipping_method:create + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show + shop:shipping_method:index + sylius:shop:shipping_method:index + shop:shipping_method:show + sylius:shop:shipping_method:show - admin:shipping_method:read - shop:shipping_method:read + shop:shipping_method:index + sylius:shop:shipping_method:index + shop:shipping_method:show + sylius:shop:shipping_method:show - shop:shipping_method:read + shop:shipping_method:index + sylius:shop:shipping_method:index + shop:shipping_method:show + sylius:shop:shipping_method:show admin:shipping_method:create - admin:shipping_method:read + sylius:admin:shipping_method:create + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show admin:shipping_method:update - shop:shipping_method:read + sylius:admin:shipping_method:update + shop:shipping_method:index + sylius:shop:shipping_method:index + shop:shipping_method:show + sylius:shop:shipping_method:show admin:shipping_method:create - admin:shipping_method:read + sylius:admin:shipping_method:create + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show admin:shipping_method:update + sylius:admin:shipping_method:update admin:shipping_method:create - admin:shipping_method:read + sylius:admin:shipping_method:create + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show admin:shipping_method:update + sylius:admin:shipping_method:update admin:shipping_method:create - admin:shipping_method:read + sylius:admin:shipping_method:create + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show admin:shipping_method:update + sylius:admin:shipping_method:update - + admin:shipping_method:create - admin:shipping_method:read + sylius:admin:shipping_method:create + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show admin:shipping_method:update + sylius:admin:shipping_method:update - + admin:shipping_method:create - admin:shipping_method:read + sylius:admin:shipping_method:create + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show admin:shipping_method:update + sylius:admin:shipping_method:update + + + + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show + admin:shipping_method:create + sylius:admin:shipping_method:create + admin:shipping_method:update + sylius:admin:shipping_method:update - admin:shipping_method:read + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show - admin:shipping_method:read + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show - admin:shipping_method:read + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodRule.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodRule.xml new file mode 100644 index 0000000000..f47a1c4533 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodRule.xml @@ -0,0 +1,46 @@ + + + + + + + + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show + + + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show + admin:shipping_method:create + sylius:admin:shipping_method:create + admin:shipping_method:update + sylius:admin:shipping_method:update + + + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show + admin:shipping_method:create + sylius:admin:shipping_method:create + admin:shipping_method:update + sylius:admin:shipping_method:update + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodTranslation.xml index adf76c7f29..d021fe4c2a 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShippingMethodTranslation.xml @@ -17,30 +17,39 @@ > - admin:shipping_method:read - shop:shipping_method:read + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show admin:shipping_method:create - admin:shipping_method:read + sylius:admin:shipping_method:create + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show admin:shipping_method:update - shop:cart:read - shop:order:account:read - shop:shipping_method:read - shop:shipment:read + sylius:admin:shipping_method:update admin:shipping_method:create - admin:shipping_method:read + sylius:admin:shipping_method:create + admin:shipping_method:index + sylius:admin:shipping_method:index + admin:shipping_method:show + sylius:admin:shipping_method:show admin:shipping_method:update - shop:shipping_method:read + sylius:admin:shipping_method:update admin:shipping_method:create + sylius:admin:shipping_method:create admin:shipping_method:update + sylius:admin:shipping_method:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopBillingData.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopBillingData.xml index 06ab5ff099..df5326cde0 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopBillingData.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopBillingData.xml @@ -17,31 +17,56 @@ > - admin:shop_billing_data:read + admin:shop_billing_data:show + sylius:admin:shop_billing_data:show admin:channel:create - admin:shop_billing_data:read + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + admin:shop_billing_data:show + sylius:admin:shop_billing_data:show admin:channel:create - admin:shop_billing_data:read + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + admin:shop_billing_data:show + sylius:admin:shop_billing_data:show admin:channel:create - admin:shop_billing_data:read + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + admin:shop_billing_data:show + sylius:admin:shop_billing_data:show admin:channel:create - admin:shop_billing_data:read + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + admin:shop_billing_data:show + sylius:admin:shop_billing_data:show admin:channel:create - admin:shop_billing_data:read + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + admin:shop_billing_data:show + sylius:admin:shop_billing_data:show admin:channel:create - admin:shop_billing_data:read + sylius:admin:channel:create + admin:channel:update + sylius:admin:channel:update + admin:shop_billing_data:show + sylius:admin:shop_billing_data:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopUser.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopUser.xml index bd8a6ef74d..54b6ef1344 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopUser.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ShopUser.xml @@ -16,8 +16,38 @@ xsi:schemaLocation="http://symfony.com/schema/dic/serializer-mapping https://symfony.com/schema/dic/serializer-mapping/serializer-mapping-1.0.xsd" > + + admin:customer:create + sylius:admin:customer:create + admin:customer:update + sylius:admin:customer:update + + + + admin:customer:create + sylius:admin:customer:create + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + admin:customer:update + sylius:admin:customer:update + + - shop:customer:read + admin:customer:index + sylius:admin:customer:index + admin:customer:show + sylius:admin:customer:show + shop:customer:show + sylius:shop:customer:show + + + + admin:customer:create + sylius:admin:customer:create + admin:customer:update + sylius:admin:customer:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxCategory.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxCategory.xml index 27a09a9c68..cbd73b0de3 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxCategory.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxCategory.xml @@ -17,27 +17,50 @@ > - admin:tax_category:read + admin:tax_category:index + sylius:admin:tax_category:index + admin:tax_category:show + sylius:admin:tax_category:show - admin:tax_category:read + admin:tax_category:index + sylius:admin:tax_category:index + admin:tax_category:show + sylius:admin:tax_category:show - admin:tax_category:read + admin:tax_category:index + sylius:admin:tax_category:index + admin:tax_category:show + sylius:admin:tax_category:show admin:tax_category:create - admin:tax_category:read + sylius:admin:tax_category:create + admin:tax_category:index + sylius:admin:tax_category:index + admin:tax_category:show + sylius:admin:tax_category:show admin:tax_category:create - admin:tax_category:read + sylius:admin:tax_category:create + admin:tax_category:index + sylius:admin:tax_category:index + admin:tax_category:show + sylius:admin:tax_category:show admin:tax_category:update + sylius:admin:tax_category:update admin:tax_category:create - admin:tax_category:read + sylius:admin:tax_category:create + admin:tax_category:index + sylius:admin:tax_category:index + admin:tax_category:show + sylius:admin:tax_category:show admin:tax_category:update + sylius:admin:tax_category:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxRate.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxRate.xml new file mode 100644 index 0000000000..4d6327a257 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxRate.xml @@ -0,0 +1,126 @@ + + + + + + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + admin:tax_rate:create + sylius:admin:tax_rate:create + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + admin:tax_rate:create + sylius:admin:tax_rate:create + admin:tax_rate:update + sylius:admin:tax_rate:update + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + admin:tax_rate:create + sylius:admin:tax_rate:create + admin:tax_rate:update + sylius:admin:tax_rate:update + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + admin:tax_rate:create + sylius:admin:tax_rate:create + admin:tax_rate:update + sylius:admin:tax_rate:update + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + admin:tax_rate:create + sylius:admin:tax_rate:create + admin:tax_rate:update + sylius:admin:tax_rate:update + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + admin:tax_rate:create + sylius:admin:tax_rate:create + admin:tax_rate:update + sylius:admin:tax_rate:update + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + admin:tax_rate:create + sylius:admin:tax_rate:create + admin:tax_rate:update + sylius:admin:tax_rate:update + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + admin:tax_rate:create + sylius:admin:tax_rate:create + admin:tax_rate:update + sylius:admin:tax_rate:update + + + admin:tax_rate:index + sylius:admin:tax_rate:index + admin:tax_rate:show + sylius:admin:tax_rate:show + admin:tax_rate:create + sylius:admin:tax_rate:create + admin:tax_rate:update + sylius:admin:tax_rate:update + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Taxon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Taxon.xml index ed6d978e26..83063019cf 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Taxon.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Taxon.xml @@ -1,34 +1,114 @@ + + - admin:taxon:read - shop:taxon:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + shop:taxon:index + sylius:shop:taxon:index + shop:taxon:show + sylius:shop:taxon:show - admin:taxon:read - shop:taxon:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + shop:taxon:index + sylius:shop:taxon:index + shop:taxon:show + sylius:shop:taxon:show admin:taxon:create - admin:taxon:update + sylius:admin:taxon:create - shop:taxon:read + shop:taxon:index + sylius:shop:taxon:index + shop:taxon:show + sylius:shop:taxon:show - shop:taxon:read + shop:taxon:index + sylius:shop:taxon:index + shop:taxon:show + sylius:shop:taxon:show - shop:taxon:read + shop:taxon:index + sylius:shop:taxon:index + shop:taxon:show + sylius:shop:taxon:show - admin:taxon:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + admin:taxon:create + sylius:admin:taxon:create + admin:taxon:update + sylius:admin:taxon:update + + + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + admin:taxon:create + sylius:admin:taxon:create + admin:taxon:update + sylius:admin:taxon:update - shop:taxon:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + shop:taxon:index + sylius:shop:taxon:index + shop:taxon:show + sylius:shop:taxon:show + + + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + admin:taxon:create + sylius:admin:taxon:create + admin:taxon:update + sylius:admin:taxon:update + + + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + admin:taxon:update + sylius:admin:taxon:update + + + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxonImage.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxonImage.xml new file mode 100644 index 0000000000..795f97e00e --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxonImage.xml @@ -0,0 +1,61 @@ + + + + + + + + admin:taxon_image:index + sylius:admin:taxon_image:index + admin:taxon_image:show + sylius:admin:taxon_image:show + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + + + + admin:taxon_image:index + sylius:admin:taxon_image:index + admin:taxon_image:show + sylius:admin:taxon_image:show + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + + + + admin:taxon_image:index + sylius:admin:taxon_image:index + admin:taxon_image:show + sylius:admin:taxon_image:show + + + + admin:taxon_image:index + sylius:admin:taxon_image:index + admin:taxon_image:show + sylius:admin:taxon_image:show + admin:taxon_image:update + sylius:admin:taxon_image:update + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxonTranslation.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxonTranslation.xml index a1b874a93a..c8b76317bb 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxonTranslation.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/TaxonTranslation.xml @@ -1,33 +1,70 @@ + + - admin:taxon:read - shop:taxon:read - admin:taxon_translation:read - shop:taxon_translation:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + admin:taxon_translation:show + sylius:admin:taxon_translation:show - admin:taxon:read - shop:taxon:read - admin:taxon_translation:read - shop:taxon_translation:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + admin:taxon_translation:show + sylius:admin:taxon_translation:show + admin:taxon:create + sylius:admin:taxon:create + admin:taxon:update + sylius:admin:taxon:update - admin:taxon:read - shop:taxon:read - admin:taxon_translation:read - shop:taxon_translation:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + admin:taxon_translation:show + sylius:admin:taxon_translation:show + admin:taxon:create + sylius:admin:taxon:create + admin:taxon:update + sylius:admin:taxon:update + + + admin:taxon:create + sylius:admin:taxon:create + admin:taxon:update + sylius:admin:taxon:update - admin:taxon:read - shop:taxon:read - admin:taxon_translation:read - shop:taxon_translation:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + admin:taxon_translation:show + sylius:admin:taxon_translation:show + admin:taxon:create + sylius:admin:taxon:create + admin:taxon:update + sylius:admin:taxon:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Zone.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Zone.xml index 3dd6e5b706..a06c8064c6 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Zone.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/Zone.xml @@ -17,35 +17,66 @@ > - admin:zone:read + admin:zone:index + sylius:admin:zone:index + admin:zone:show + sylius:admin:zone:show admin:zone:create - admin:zone:read + sylius:admin:zone:create + admin:zone:index + sylius:admin:zone:index + admin:zone:show + sylius:admin:zone:show admin:zone:create - admin:zone:read + sylius:admin:zone:create + admin:zone:index + sylius:admin:zone:index + admin:zone:show + sylius:admin:zone:show admin:zone:update + sylius:admin:zone:update admin:zone:create - admin:zone:read + sylius:admin:zone:create + admin:zone:index + sylius:admin:zone:index + admin:zone:show + sylius:admin:zone:show admin:zone:create - admin:zone:read + sylius:admin:zone:create + admin:zone:index + sylius:admin:zone:index + admin:zone:show + sylius:admin:zone:show - admin:zone:read + admin:zone:index + sylius:admin:zone:index + admin:zone:show + sylius:admin:zone:show - admin:zone:read + admin:zone:index + sylius:admin:zone:index + admin:zone:show + sylius:admin:zone:show admin:zone:create - admin:zone:read + sylius:admin:zone:create + admin:zone:index + sylius:admin:zone:index + admin:zone:show + sylius:admin:zone:show admin:zone:update + sylius:admin:zone:update diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ZoneMember.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ZoneMember.xml index a2811e10c3..3dffccf529 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ZoneMember.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/serialization/ZoneMember.xml @@ -17,12 +17,20 @@ > - admin:zone_member:read + admin:zone_member:show + sylius:admin:zone_member:show admin:zone:create + sylius:admin:zone:create admin:zone:update - admin:zone_member:read + sylius:admin:zone:update + admin:zone_member:show + sylius:admin:zone_member:show + + + admin:zone_member:show + sylius:admin:zone_member:show diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services.xml index 9a85576caa..0e37411843 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services.xml @@ -24,21 +24,6 @@ - - - - - - - - - - - - - - - @@ -89,13 +74,9 @@ - - - - - - - + + + @@ -122,7 +103,7 @@ - + @@ -141,6 +122,8 @@ + + - + @@ -169,5 +152,14 @@ + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/applicators.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/applicators.xml index 9ee3efa0de..c703fb9012 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/applicators.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/applicators.xml @@ -23,15 +23,20 @@ - + - + - + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/command_handlers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/command_handlers.xml index e5a7dc79ab..26d9885c5e 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/command_handlers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/command_handlers.xml @@ -77,7 +77,7 @@ - + @@ -86,7 +86,7 @@ - + @@ -94,7 +94,7 @@ - + @@ -104,7 +104,7 @@ - + @@ -175,7 +175,7 @@ - + @@ -183,29 +183,29 @@ - + - + - + - + @@ -213,13 +213,27 @@ + - + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/context_builders.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/context_builders.xml index 97b341e9ee..a5292631a7 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/context_builders.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/context_builders.xml @@ -27,6 +27,16 @@ + + + %sylius_api.serialization_groups.skip_adding_read_group% + %sylius_api.serialization_groups.skip_adding_index_and_show_groups% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %sylius_core.orders_statistics.intervals_map% + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/creators.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/creators.xml new file mode 100644 index 0000000000..c0fdb2e7ea --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/creators.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml index f0df0746e7..2cff1c5bd4 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_persisters.xml @@ -31,6 +31,12 @@ + + + + + + @@ -42,15 +48,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_providers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_providers.xml index e9cf591ece..5162b76afd 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_providers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/data_providers.xml @@ -110,5 +110,11 @@ + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/event_subscribers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/event_subscribers.xml index 8b07ee7751..6e6b43702b 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/event_subscribers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/event_subscribers.xml @@ -43,5 +43,21 @@ + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml index 8cac075a00..77ab980ce5 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/extensions.xml @@ -43,6 +43,11 @@ + + + + + @@ -98,11 +103,13 @@ + %sylius.api.doctrine_extension.order_shop_user_item.filter_cart.allowed_non_get_operations% + %sylius.api.doctrine_extension.order_visitor_item.filter_cart.allowed_non_get_operations% @@ -111,10 +118,32 @@ + + + + + + + + + %sylius_api.order_states_to_filter_out% + + + + + + + + + + + %sylius.model.promotion.class% + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml index 854f235443..5edc5c55d3 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/filters.xml @@ -33,6 +33,14 @@ + + + partial + partial + + + + exact @@ -42,7 +50,7 @@ - + @@ -50,6 +58,13 @@ + + + exact + + + + partial @@ -64,6 +79,13 @@ + + + + + + + exact @@ -85,6 +107,55 @@ + + + exclude_null + + + + + + + exact + + + + + + + exact + + + + + + + exact + + + + + + + exact + + + + + + + exact + + + + + + + + + + + @@ -100,6 +171,13 @@ + + + + + + + partial @@ -119,13 +197,13 @@ - + - + @@ -169,10 +247,168 @@ - - + + + exclude_null + + + + + + + exclude_null + + + + + + + + + + + + + + + + + + exact + + + + + + + exact + + + + + + + + + + + + + + exact + + + + + + + exact + + + + + + + exact + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + exact + + + + + + + exact + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + exact + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml new file mode 100644 index 0000000000..be438a3f5c --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/query_handlers.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml index 625a132138..0318a20290 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/serializers.xml @@ -26,9 +26,15 @@ - + + + + + + + @@ -39,8 +45,17 @@ - - + + + + + + + + + + + @@ -62,18 +77,11 @@ - - - - - - - - + @@ -93,7 +101,12 @@ - + + + + + + @@ -103,5 +116,40 @@ + + + + + + %sylius.form.type.channel_price_history_config.validation_groups% + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/services/validator.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/services/validator.xml index 1d882eaa0a..ff164d61ad 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/services/validator.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/services/validator.xml @@ -48,7 +48,7 @@ - + @@ -70,7 +70,7 @@ - + @@ -90,6 +90,11 @@ + + + + + @@ -110,11 +115,26 @@ + + + + + + + + %sylius.shop_user.token.password_reset.ttl% + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AccountResetPassword.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AccountResetPassword.xml index ac7615077f..68615ab35f 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AccountResetPassword.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AccountResetPassword.xml @@ -45,5 +45,16 @@ + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AddItemToCart.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AddItemToCart.xml index 975424d13b..fde42729a4 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AddItemToCart.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AddItemToCart.xml @@ -18,7 +18,17 @@ sylius + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AddProductReview.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AddProductReview.xml index 59d446e15e..92285bd629 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AddProductReview.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/AddProductReview.xml @@ -23,9 +23,7 @@ - - - + @@ -54,6 +52,14 @@ + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ChangeItemQuantityInCart.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ChangeItemQuantityInCart.xml index 5eaaab51fb..e3b4dacc80 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ChangeItemQuantityInCart.xml +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ChangeItemQuantityInCart.xml @@ -24,6 +24,10 @@ + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ChangePaymentMethod.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ChangePaymentMethod.xml new file mode 100644 index 0000000000..04b88717e2 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ChangePaymentMethod.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/Channel.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/Channel.xml new file mode 100644 index 0000000000..eae9231cdb --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/Channel.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/Customer.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/Customer.xml new file mode 100644 index 0000000000..bd7e7d5dce --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/Customer.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/GeneratePromotionCoupon.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/GeneratePromotionCoupon.xml new file mode 100644 index 0000000000..61d145d0df --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/GeneratePromotionCoupon.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ProductVariant.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ProductVariant.xml new file mode 100644 index 0000000000..395e13d3db --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ProductVariant.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ShopBillingData.xml b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ShopBillingData.xml new file mode 100644 index 0000000000..0a77587ba1 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Resources/config/validation/ShopBillingData.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Resources/translations/validators.en.yaml b/src/Sylius/Bundle/ApiBundle/Resources/translations/validators.en.yaml index b0bda0ec47..8d801e7e21 100644 --- a/src/Sylius/Bundle/ApiBundle/Resources/translations/validators.en.yaml +++ b/src/Sylius/Bundle/ApiBundle/Resources/translations/validators.en.yaml @@ -6,22 +6,36 @@ sylius: is_verified: 'Account with email %email% is currently verified.' address: without_country: 'The address without country cannot exist' + code: + not_blank: 'Please enter a code.' + invalid: 'The code should contain only letters, numbers, dashes ("-" symbol) and underscores ("_" symbol).' contact_request: email_is_invalid: 'The provided email is invalid.' country: not_exist: 'The country %countryCode% does not exist.' + date_interval: + empty: 'The date interval cannot be empty.' + invalid: 'The date interval is not valid ISO 8601 interval.' + date_time: + invalid: 'The date time is not valid ISO 8601 date time in Y-m-d\TH:i:s format.' order: not_empty: 'An empty order cannot be completed.' payment_method: + cannot_change_payment_method_for_cancelled_order: 'You cannot change the payment method for a cancelled order.' not_available: 'The payment method %name% is not available for this order. Please choose another one.' not_exist: 'The payment method with %code% code does not exist.' product: not_exist: 'The product %productName% does not exist.' product_variant: - not_exist: 'The product variant with %productVariantCode% does not exist.' + not_exist: 'The product variant %productVariantCode% does not exist.' not_longer_available: 'The product variant with name %productVariantName% does not exist.' not_sufficient: 'The product variant with %productVariantCode% code does not have sufficient stock.' product_variant_with_name_not_sufficient: 'The product variant with %productVariantName% name does not have sufficient stock.' + option_values: + single_value: 'The product variant can have only one value configured for a single option.' + reset_password: + invalid_token: 'Password reset token %token% is invalid.' + token_expired: 'Password reset token has expired.' shipment: not_found: 'The shipment does not exist.' shipped: 'You cannot ship a shipment that was shipped before.' @@ -29,3 +43,6 @@ sylius: not_found: 'The shipping method with %code% code does not exist.' not_available: 'The shipping method %name% is not available for this order. Please reselect your shipping method.' shipping_address_not_found: 'Order should be addressed first.' + statistics: + end_date: + invalid: 'The start date must be earlier than the end date.' diff --git a/src/Sylius/Bundle/ApiBundle/Resources/views/SwaggerUi/index.html.twig b/src/Sylius/Bundle/ApiBundle/Resources/views/SwaggerUi/index.html.twig deleted file mode 100644 index 5e0f4516f7..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Resources/views/SwaggerUi/index.html.twig +++ /dev/null @@ -1,184 +0,0 @@ - - - - - {% if title %}{{ title }} - {% endif %}API Platform - - {% block stylesheet %} - - - - {% endblock %} - - {% set oauth_data = {'oauth': swagger_data.oauth|merge({'redirectUrl' : absolute_url(asset('bundles/apiplatform/swagger-ui/oauth2-redirect.html')) })} %} - {# json_encode(65) is for JSON_UNESCAPED_SLASHES|JSON_HEX_TAG to avoid JS XSS #} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
EXPERIMENTAL
-
-
- -{% if showWebby %} -
-
-{% endif %} -
- -
-
-
- Available formats: - {% for format in formats|keys %} - {{ format }} - {% endfor %} -
- Other API docs: - {% set active_ui = app.request.get('ui', 'swagger_ui') %} - {% if swaggerUiEnabled and active_ui != 'swagger_ui' %}Swagger UI{% endif %} - {% if reDocEnabled and active_ui != 're_doc' %}ReDoc{% endif %} - {% if not graphqlEnabled %}GraphiQL{% endif %} - {% if graphiQlEnabled %}GraphiQL{% endif %} - {% if graphQlPlaygroundEnabled %}GraphQL Playground{% endif %} -
-
-
- -{% block javascript %} - {% if (reDocEnabled and not swaggerUiEnabled) or (reDocEnabled and 're_doc' == active_ui) %} - - - {% else %} - - - - {% endif %} -{% endblock %} - - - - diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/AddressDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/AddressDenormalizer.php index 59b8d7e5a1..bbc83c24d4 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/AddressDenormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/AddressDenormalizer.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\ApiBundle\Serializer; use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -/** @experimental */ final class AddressDenormalizer implements ContextAwareDenormalizerInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ChannelDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ChannelDenormalizer.php new file mode 100644 index 0000000000..e854e1a2c3 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ChannelDenormalizer.php @@ -0,0 +1,71 @@ + $channelPriceHistoryConfigFactory + * @param FactoryInterface $shopBillingDataFactory + */ + public function __construct( + private FactoryInterface $channelPriceHistoryConfigFactory, + private FactoryInterface $shopBillingDataFactory, + ) { + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return + !isset($context[self::ALREADY_CALLED]) && + is_array($data) && + is_a($type, ChannelInterface::class, true) + ; + } + + public function denormalize(mixed $data, string $type, string $format = null, array $context = []) + { + $context[self::ALREADY_CALLED] = true; + $data = (array) $data; + + $channel = $this->denormalizer->denormalize($data, $type, $format, $context); + Assert::isInstanceOf($channel, ChannelInterface::class); + if (null === $channel->getChannelPriceHistoryConfig()) { + /** @var ChannelPriceHistoryConfigInterface $channelPriceHistoryConfig */ + $channelPriceHistoryConfig = $this->channelPriceHistoryConfigFactory->createNew(); + $channel->setChannelPriceHistoryConfig($channelPriceHistoryConfig); + } + + if (null === $channel->getShopBillingData()) { + /** @var ShopBillingDataInterface $shopBillingData */ + $shopBillingData = $this->shopBillingDataFactory->createNew(); + $channel->setShopBillingData($shopBillingData); + } + + return $channel; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ChannelPriceHistoryConfigDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ChannelPriceHistoryConfigDenormalizer.php new file mode 100644 index 0000000000..95076c0de2 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ChannelPriceHistoryConfigDenormalizer.php @@ -0,0 +1,80 @@ +validateData($data); + + $channelPriceHistoryConfig = $this->denormalizer->denormalize($data, $type, $format, $context); + Assert::isInstanceOf($channelPriceHistoryConfig, ChannelPriceHistoryConfigInterface::class); + $channelPriceHistoryConfig->clearTaxonsExcludedFromShowingLowestPrice(); + + foreach ($data['taxonsExcludedFromShowingLowestPrice'] ?? [] as $excludedTaxonIri) { + /** @var TaxonInterface $taxon */ + $taxon = $this->iriConverter->getResourceFromIri($excludedTaxonIri); + + $channelPriceHistoryConfig->addTaxonExcludedFromShowingLowestPrice($taxon); + } + + return $channelPriceHistoryConfig; + } + + private function validateData(array $data): void + { + /** @var ChannelPriceHistoryConfigInterface $dummyConfig */ + $dummyConfig = $this->channelPriceHistoryConfigFactory->createNew(); + $this->validator->validate( + $dummyConfig, + $data, + $this->validationGroups, + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/CommandArgumentsDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/CommandArgumentsDenormalizer.php index 4c30b719d1..421e33107b 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/CommandArgumentsDenormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/CommandArgumentsDenormalizer.php @@ -19,11 +19,10 @@ use Sylius\Bundle\ApiBundle\Converter\IriToIdentifierConverterInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -/** @experimental */ final class CommandArgumentsDenormalizer implements ContextAwareDenormalizerInterface { public function __construct( - private DenormalizerInterface $objectNormalizer, + private DenormalizerInterface $commandDenormalizer, private IriToIdentifierConverterInterface $iriToIdentifierConverter, private DataTransformerInterface $commandAwareInputDataTransformer, ) { @@ -38,13 +37,7 @@ final class CommandArgumentsDenormalizer implements ContextAwareDenormalizerInte return false; } - foreach (class_implements($inputClassName) as $classInterface) { - if ($classInterface === IriToIdentifierConversionAwareInterface::class) { - return true; - } - } - - return false; + return is_subclass_of($inputClassName, IriToIdentifierConversionAwareInterface::class); } public function denormalize($data, $type, $format = null, array $context = []) @@ -52,29 +45,41 @@ final class CommandArgumentsDenormalizer implements ContextAwareDenormalizerInte /** @var class-string $inputClassName */ $inputClassName = $this->getInputClassName($context); - foreach (class_implements($inputClassName) as $classInterface) { - if ($classInterface !== IriToIdentifierConversionAwareInterface::class) { - continue; - } - - foreach ($data as $classFieldName => $classFieldValue) { - if ($this->iriToIdentifierConverter->isIdentifier($data[$classFieldName]) && $data[$classFieldName] != '') { - $data[$classFieldName] = $this->iriToIdentifierConverter->getIdentifier((string) $data[$classFieldName]); - } - } + if (is_subclass_of($inputClassName, IriToIdentifierConversionAwareInterface::class)) { + $data = $this->convertIrisToIdentifiers($data); } - $denormalizedInput = $this->objectNormalizer->denormalize($data, $this->getInputClassName($context), $format, $context); + $denormalizedCommand = $this->commandDenormalizer->denormalize($data, $inputClassName, $format, $context); - if ($this->commandAwareInputDataTransformer->supportsTransformation($denormalizedInput, $type, $context)) { - return $this->commandAwareInputDataTransformer->transform($denormalizedInput, $type, $context); + if ($this->commandAwareInputDataTransformer->supportsTransformation($denormalizedCommand, $type, $context)) { + return $this->commandAwareInputDataTransformer->transform($denormalizedCommand, $type, $context); } - return $denormalizedInput; + return $denormalizedCommand; } private function getInputClassName(array $context): ?string { return $context['input']['class'] ?? null; } + + /** + * @param array|string|int|mixed $data + * + * @return array|string|int|mixed + */ + private function convertIrisToIdentifiers(mixed $data): mixed + { + if (is_string($data) && $data !== '' && $this->iriToIdentifierConverter->isIdentifier($data)) { + return $this->iriToIdentifierConverter->getIdentifier($data); + } + + if (is_array($data)) { + foreach ($data as $classFieldName => $classFieldValue) { + $data[$classFieldName] = $this->convertIrisToIdentifiers($classFieldValue); + } + } + + return $data; + } } diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/CommandDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/CommandDenormalizer.php index d9ed90f8a2..419c2c89fa 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/CommandDenormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/CommandDenormalizer.php @@ -13,19 +13,19 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Serializer; +use Sylius\Bundle\ApiBundle\Exception\InvalidRequestArgumentException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; -/** @experimental */ final class CommandDenormalizer implements ContextAwareDenormalizerInterface { - private const OBJECT_TO_POPULATE = 'object_to_populate'; - public function __construct( private DenormalizerInterface $itemNormalizer, - private NameConverterInterface $nameConverter, + private AdvancedNameConverterInterface $nameConverter, ) { } @@ -36,39 +36,36 @@ final class CommandDenormalizer implements ContextAwareDenormalizerInterface public function denormalize($data, $type, $format = null, array $context = []) { - if (isset($context[self::OBJECT_TO_POPULATE])) { + try { return $this->itemNormalizer->denormalize($data, $type, $format, $context); + } catch (UnexpectedValueException $exception) { + $previousException = $exception->getPrevious(); + if ($previousException instanceof NotNormalizableValueException) { + throw new InvalidRequestArgumentException( + sprintf( + 'Request field "%s" should be of type "%s".', + $this->normalizeFieldName($previousException->getPath(), $context['input']['class']), + implode(', ', $previousException->getExpectedTypes()), + ), + ); + } + + throw $exception; + } catch (MissingConstructorArgumentsException $exception) { + $class = $context['input']['class']; + + throw new MissingConstructorArgumentsException(sprintf( + 'Request does not have the following required fields specified: %s.', + implode(', ', array_map( + fn (string $field) => $this->normalizeFieldName($field, $class), + $exception->getMissingConstructorArguments(), + )), + )); } - - $class = $context['input']['class']; - $constructor = (new \ReflectionClass($class))->getConstructor(); - - if (null !== $constructor) { - $this->assertConstructorArgumentsPresence($constructor, $class, $data); - } - - return $this->itemNormalizer->denormalize($data, $type, $format, $context); } - private function assertConstructorArgumentsPresence( - \ReflectionMethod $constructor, - string $class, - mixed $data, - ): void { - $parameters = $constructor->getParameters(); - - $missingFields = []; - foreach ($parameters as $parameter) { - $name = $this->nameConverter->normalize($parameter->getName(), $class); - if (!isset($data[$name]) && !($parameter->allowsNull() || $parameter->isDefaultValueAvailable())) { - $missingFields[] = $name; - } - } - - if (count($missingFields) > 0) { - throw new MissingConstructorArgumentsException( - sprintf('Request does not have the following required fields specified: %s.', implode(', ', $missingFields)), - ); - } + private function normalizeFieldName(string $field, string $class): string + { + return $this->nameConverter->normalize($field, $class); } } diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/CommandNormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/CommandNormalizer.php index 486c35b7db..2c5b6d83d5 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/CommandNormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/CommandNormalizer.php @@ -13,13 +13,11 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Serializer; +use Sylius\Bundle\ApiBundle\Exception\InvalidRequestArgumentException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -/** - * @experimental - */ final class CommandNormalizer implements ContextAwareNormalizerInterface { private const ALREADY_CALLED = 'sylius_command_normalizer_already_called'; @@ -37,7 +35,7 @@ final class CommandNormalizer implements ContextAwareNormalizerInterface return is_object($data) && method_exists($data, 'getClass') && - $data->getClass() === MissingConstructorArgumentsException::class + ($data->getClass() === MissingConstructorArgumentsException::class || $data->getClass() === InvalidRequestArgumentException::class) ; } diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ContextKeys.php b/src/Sylius/Bundle/ApiBundle/Serializer/ContextKeys.php index df395ac679..ab031579a4 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ContextKeys.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ContextKeys.php @@ -13,7 +13,6 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Serializer; -/** @experimental */ class ContextKeys { public const CHANNEL = 'sylius_api_channel'; diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/CustomerDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/CustomerDenormalizer.php new file mode 100644 index 0000000000..392fc35ae0 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/CustomerDenormalizer.php @@ -0,0 +1,53 @@ +calendar->now()->format(\DateTimeInterface::RFC3339) : null; + } + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/DoctrineCollectionValuesNormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/DoctrineCollectionValuesNormalizer.php new file mode 100644 index 0000000000..0094d8ba5e --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/DoctrineCollectionValuesNormalizer.php @@ -0,0 +1,39 @@ +normalizer->normalize($object->getValues(), $format, $context); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/Exception/InvalidAmountTypeException.php b/src/Sylius/Bundle/ApiBundle/Serializer/Exception/InvalidAmountTypeException.php new file mode 100644 index 0000000000..c919763a8e --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/Exception/InvalidAmountTypeException.php @@ -0,0 +1,25 @@ +validateValue($data); + $data = $this->denormalizeValue($data); + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } + + /** + * @param array $data + * + * @return array + */ + private function denormalizeValue(array $data): array + { + /** @var ProductAttributeInterface $attribute */ + $attribute = $this->iriConverter->getResourceFromIri($data['attribute']); + + if (in_array($attribute->getType(), [DateAttributeType::TYPE, DateTimeAttributeType::TYPE], true)) { + $data['value'] = new \DateTime($data['value']); + } + + return $data; + } + + /** @param array $data */ + private function validateValue(array $data): void + { + $value = $data['value']; + if ($value === null) { + return; + } + + /** @var ProductAttributeInterface $attribute */ + $attribute = $this->iriConverter->getResourceFromIri($data['attribute']); + + switch ($attribute->getStorageType()) { + case AttributeValueInterface::STORAGE_BOOLEAN: + if (!is_bool($value)) { + $this->throwException($attribute->getName(), $attribute->getStorageType()); + } + + return; + case AttributeValueInterface::STORAGE_INTEGER: + if (!is_int($value)) { + $this->throwException($attribute->getName(), $attribute->getStorageType()); + } + + return; + case AttributeValueInterface::STORAGE_FLOAT: + if (!is_int($value) && !is_float($value)) { + $this->throwException($attribute->getName(), $attribute->getStorageType()); + } + + return; + case AttributeValueInterface::STORAGE_JSON: + if (!is_array($value)) { + $this->throwException($attribute->getName(), 'array'); + } + + return; + default: + if (!is_string($value)) { + $this->throwException($attribute->getName(), 'string'); + } + } + } + + private function throwException(string $attributeName, string $type): void + { + throw new InvalidProductAttributeValueTypeException(sprintf( + 'The value of attribute "%s" has an invalid type, it must be of type %s.', + $attributeName, + $type, + )); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ProductAttributeValueNormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ProductAttributeValueNormalizer.php index 10afa4a815..5e98de5081 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ProductAttributeValueNormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ProductAttributeValueNormalizer.php @@ -23,7 +23,6 @@ use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Webmozart\Assert\Assert; -/** @experimental */ final class ProductAttributeValueNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ProductDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ProductDenormalizer.php new file mode 100644 index 0000000000..db40088525 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ProductDenormalizer.php @@ -0,0 +1,74 @@ +denormalizeOptions($data, $context); + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } + + /** + * @param array $data + * @param array $context + * + * @return array + */ + private function denormalizeOptions(array $data, array $context): array + { + if (!isset($context[AbstractNormalizer::OBJECT_TO_POPULATE])) { + return $data; + } + + if (!isset($data['options'])) { + return $data; + } + + /** @var ProductInterface $product */ + $product = $context[AbstractNormalizer::OBJECT_TO_POPULATE]; + Assert::isInstanceOf($product, ProductInterface::class); + + if (!$product->getVariants()->isEmpty()) { + unset($data['options']); + } + + return $data; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ProductImageNormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ProductImageNormalizer.php index 3f44d7f82a..dbe250a79c 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ProductImageNormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ProductImageNormalizer.php @@ -21,7 +21,6 @@ use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Webmozart\Assert\Assert; -/** @experimental */ class ProductImageNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ProductNormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ProductNormalizer.php index 81999d127f..03dfc35fa8 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ProductNormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ProductNormalizer.php @@ -13,16 +13,15 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Serializer; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Sylius\Component\Core\Model\ProductInterface; -use Sylius\Component\Core\Model\ProductVariantInterface; +use Sylius\Component\Product\Model\ProductVariantInterface; use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Webmozart\Assert\Assert; -/** @experimental */ final class ProductNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; @@ -46,12 +45,12 @@ final class ProductNormalizer implements ContextAwareNormalizerInterface, Normal $data['variants'] = $object ->getEnabledVariants() - ->map(fn (ProductVariantInterface $variant): string => $this->iriConverter->getIriFromItem($variant)) + ->map(fn (ProductVariantInterface $variant): string => $this->iriConverter->getIriFromResource($variant)) ->getValues() ; $defaultVariant = $this->defaultProductVariantResolver->getVariant($object); - $data['defaultVariant'] = $defaultVariant === null ? null : $this->iriConverter->getIriFromItem($defaultVariant); + $data['defaultVariant'] = $defaultVariant === null ? null : $this->iriConverter->getIriFromResource($defaultVariant); return $data; } diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ProductVariantChannelPricingsChannelCodeKeyDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ProductVariantChannelPricingsChannelCodeKeyDenormalizer.php new file mode 100644 index 0000000000..834d5ff8a6 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ProductVariantChannelPricingsChannelCodeKeyDenormalizer.php @@ -0,0 +1,62 @@ + }> $data */ + public function denormalize(mixed $data, string $type, string $format = null, array $context = []) + { + $context[self::ALREADY_CALLED] = true; + + if (array_key_exists(self::KEY_CHANNEL_PRICINGS, $data)) { + foreach ($data[self::KEY_CHANNEL_PRICINGS] as $key => &$channelPricing) { + if (array_key_exists(self::KEY_CHANNEL_CODE, $channelPricing) && $channelPricing[self::KEY_CHANNEL_CODE] !== $key) { + throw new ChannelPricingChannelCodeMismatchException(sprintf( + 'The channelCode of channelPricing does not match the key. Key: "%s", channelCode: "%s"', + $key, + $channelPricing[self::KEY_CHANNEL_CODE], + )); + } + + $channelPricing[self::KEY_CHANNEL_CODE] = $key; + } + } + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ProductVariantNormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ProductVariantNormalizer.php index 4719da618e..5f1255ee7e 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ProductVariantNormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ProductVariantNormalizer.php @@ -13,12 +13,10 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Serializer; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Doctrine\Common\Collections\ArrayCollection; use Sylius\Bundle\ApiBundle\SectionResolver\AdminApiSection; use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface; -use Sylius\Component\Channel\Context\ChannelContextInterface; -use Sylius\Component\Channel\Context\ChannelNotFoundException; use Sylius\Component\Core\Calculator\ProductVariantPricesCalculatorInterface; use Sylius\Component\Core\Exception\MissingChannelConfigurationException; use Sylius\Component\Core\Model\CatalogPromotionInterface; @@ -30,7 +28,6 @@ use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Webmozart\Assert\Assert; -/** @experimental */ final class ProductVariantNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; @@ -39,7 +36,6 @@ final class ProductVariantNormalizer implements ContextAwareNormalizerInterface, public function __construct( private ProductVariantPricesCalculatorInterface $priceCalculator, - private ChannelContextInterface $channelContext, private AvailabilityCheckerInterface $availabilityChecker, private SectionProviderInterface $uriBasedSectionContext, private IriConverterInterface $iriConverter, @@ -56,9 +52,8 @@ final class ProductVariantNormalizer implements ContextAwareNormalizerInterface, $data['inStock'] = $this->availabilityChecker->isStockAvailable($object); - try { - $channel = $this->channelContext->getChannel(); - } catch (ChannelNotFoundException) { + $channel = $context[ContextKeys::CHANNEL] ?? null; + if (!$channel instanceof ChannelInterface) { return $data; } Assert::isInstanceOf($channel, ChannelInterface::class); @@ -66,6 +61,10 @@ final class ProductVariantNormalizer implements ContextAwareNormalizerInterface, try { $data['price'] = $this->priceCalculator->calculate($object, ['channel' => $channel]); $data['originalPrice'] = $this->priceCalculator->calculateOriginal($object, ['channel' => $channel]); + $data['lowestPriceBeforeDiscount'] = $this->priceCalculator->calculateLowestPriceBeforeDiscount( + $object, + ['channel' => $channel], + ); } catch (MissingChannelConfigurationException) { unset($data['price'], $data['originalPrice']); } @@ -74,7 +73,7 @@ final class ProductVariantNormalizer implements ContextAwareNormalizerInterface, $appliedPromotions = $object->getAppliedPromotionsForChannel($channel); if (!$appliedPromotions->isEmpty()) { $data['appliedPromotions'] = array_map( - fn (CatalogPromotionInterface $catalogPromotion) => $this->iriConverter->getIriFromItem($catalogPromotion), + fn (CatalogPromotionInterface $catalogPromotion) => $this->iriConverter->getIriFromResource($catalogPromotion), $appliedPromotions->toArray(), ); } diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ShippingMethodNormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ShippingMethodNormalizer.php index e5f617538b..e91a3cbe41 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ShippingMethodNormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ShippingMethodNormalizer.php @@ -28,7 +28,6 @@ use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Webmozart\Assert\Assert; -/** @experimental */ final class ShippingMethodNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/TaxRateDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/TaxRateDenormalizer.php new file mode 100644 index 0000000000..41620aaf86 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/TaxRateDenormalizer.php @@ -0,0 +1,52 @@ +denormalizer->denormalize($data, $type, $format, $context); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableDenormalizer.php new file mode 100644 index 0000000000..7e5dbea294 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableDenormalizer.php @@ -0,0 +1,70 @@ +localeProvider->getDefaultLocaleCode(); + + if (!$this->hasDefaultTranslation($data['translations'] ?? [], $defaultLocaleCode)) { + $data['translations'][$defaultLocaleCode] = [ + 'locale' => $defaultLocaleCode, + ]; + } + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return + Request::METHOD_POST === ($context[ContextKeys::HTTP_REQUEST_METHOD_TYPE] ?? null) && + !isset($context[self::getAlreadyCalledKey($type)]) && + is_a($type, TranslatableInterface::class, true) + ; + } + + private static function getAlreadyCalledKey(string $class): string + { + return sprintf(self::ALREADY_CALLED, $class); + } + + private function hasDefaultTranslation(array $translations, string $defaultLocale): bool + { + return + isset($translations[$defaultLocale]['locale']) && + $defaultLocale === $translations[$defaultLocale]['locale'] + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableLocaleKeyDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableLocaleKeyDenormalizer.php new file mode 100644 index 0000000000..a290ed9c35 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Serializer/TranslatableLocaleKeyDenormalizer.php @@ -0,0 +1,63 @@ + }> $data */ + public function denormalize(mixed $data, string $type, string $format = null, array $context = []) + { + $context[self::getAlreadyCalledKey($type)] = true; + + if (array_key_exists('translations', $data)) { + foreach ($data['translations'] as $key => &$translation) { + if (array_key_exists('locale', $translation) && $translation['locale'] !== $key) { + throw new TranslationLocaleMismatchException(sprintf( + 'The locale of translation does not match the key. Key: "%s", locale: "%s"', + $key, + $translation['locale'], + )); + } + + $translation['locale'] = $key; + } + } + + return $this->denormalizer->denormalize($data, $type, $format, $context); + } + + private static function getAlreadyCalledKey(string $class): string + { + return sprintf(self::ALREADY_CALLED, $class); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Serializer/ZoneDenormalizer.php b/src/Sylius/Bundle/ApiBundle/Serializer/ZoneDenormalizer.php index e4b3f2e216..294aacc7e3 100644 --- a/src/Sylius/Bundle/ApiBundle/Serializer/ZoneDenormalizer.php +++ b/src/Sylius/Bundle/ApiBundle/Serializer/ZoneDenormalizer.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Serializer; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Selectable; @@ -25,7 +25,6 @@ use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; use Webmozart\Assert\Assert; -/** @experimental */ final class ZoneDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface { use DenormalizerAwareTrait; @@ -67,7 +66,7 @@ final class ZoneDenormalizer implements ContextAwareDenormalizerInterface, Denor if (isset($member['code']) && in_array($member['code'], $membersCodes)) { unset($data['members'][$key]); - $data['members'][$key] = $this->iriConverter->getIriFromItem( + $data['members'][$key] = $this->iriConverter->getIriFromResource( $this->getZoneMemberByCode($members, $member['code']), ); } diff --git a/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/ChannelContextBuilder.php b/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/ChannelContextBuilder.php index cbbd08c2b7..d40e7a89f3 100644 --- a/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/ChannelContextBuilder.php +++ b/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/ChannelContextBuilder.php @@ -19,7 +19,6 @@ use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Channel\Context\ChannelNotFoundException; use Symfony\Component\HttpFoundation\Request; -/** @experimental */ final class ChannelContextBuilder implements SerializerContextBuilderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/HttpRequestMethodTypeContextBuilder.php b/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/HttpRequestMethodTypeContextBuilder.php index ed54c472eb..823e8981c5 100644 --- a/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/HttpRequestMethodTypeContextBuilder.php +++ b/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/HttpRequestMethodTypeContextBuilder.php @@ -17,7 +17,6 @@ use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface; use Sylius\Bundle\ApiBundle\Serializer\ContextKeys; use Symfony\Component\HttpFoundation\Request; -/** @experimental */ final class HttpRequestMethodTypeContextBuilder implements SerializerContextBuilderInterface { public function __construct(private SerializerContextBuilderInterface $decoratedLocaleBuilder) diff --git a/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/LocaleContextBuilder.php b/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/LocaleContextBuilder.php index 5f8114c66d..ca4e76db90 100644 --- a/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/LocaleContextBuilder.php +++ b/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/LocaleContextBuilder.php @@ -19,7 +19,6 @@ use Sylius\Component\Locale\Context\LocaleContextInterface; use Sylius\Component\Locale\Context\LocaleNotFoundException; use Symfony\Component\HttpFoundation\Request; -/** @experimental */ final class LocaleContextBuilder implements SerializerContextBuilderInterface { public function __construct( diff --git a/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/ReadOperationContextBuilder.php b/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/ReadOperationContextBuilder.php new file mode 100644 index 0000000000..b5ff2d744f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/SerializerContextBuilder/ReadOperationContextBuilder.php @@ -0,0 +1,83 @@ + $extractedAttributes + * + * @return array + */ + public function createFromRequest(Request $request, bool $normalization, array $extractedAttributes = null): array + { + $context = $this->decorated->createFromRequest($request, $normalization, $extractedAttributes); + + $groups = $context['groups'] ?? []; + $groups = is_string($groups) ? [$groups] : $groups; + + if ($groups === []) { + return $context; + } + + foreach ($groups as $group) { + if ($this->shouldReadGroupBeAdded($group) && !$this->skipAddingReadGroup) { + $readGroup = str_replace([':show', ':index'], ':read', $group); + + if (in_array($readGroup, $groups, true)) { + continue; + } + + $groups[] = $readGroup; + } + + if ($this->shouldIndexAndShowGroupsBeAdded($group) && !$this->skipAddingIndexAndShowGroups) { + $indexGroup = str_replace(':read', ':index', $group); + $showGroup = str_replace(':read', ':show', $group); + + if (!in_array($indexGroup, $groups, true)) { + $groups[] = $indexGroup; + } + + if (!in_array($showGroup, $groups, true)) { + $groups[] = $showGroup; + } + } + } + + $context['groups'] = $groups; + + return $context; + } + + private function shouldReadGroupBeAdded(string $group): bool + { + return str_ends_with($group, ':show') || str_ends_with($group, ':index'); + } + + private function shouldIndexAndShowGroupsBeAdded(string $group): bool + { + return str_ends_with($group, ':read'); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Swagger/AcceptLanguageHeaderDocumentationNormalizer.php b/src/Sylius/Bundle/ApiBundle/Swagger/AcceptLanguageHeaderDocumentationNormalizer.php deleted file mode 100644 index cedf3f8808..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Swagger/AcceptLanguageHeaderDocumentationNormalizer.php +++ /dev/null @@ -1,65 +0,0 @@ -decoratedNormalizer->supportsNormalization($data, $format); - } - - public function normalize($object, $format = null, array $context = []) - { - $docs = $this->decoratedNormalizer->normalize($object, $format, $context); - - $acceptLanguageHeader = [ - 'name' => 'Accept-Language', - 'in' => 'header', - 'required' => false, - 'description' => 'Locales in this enum are all locales defined in the shop and only enabled ones will work in the given channel in the shop.', - 'schema' => [ - 'type' => 'string', - 'enum' => array_map( - fn (LocaleInterface $locale): string => $locale->getCode(), - $this->localeRepository->findAll(), - ), - ], - ]; - - foreach ($docs['paths'] as $path => $methods) { - foreach ($methods as $methodName => $methodBody) { - if (is_object($methodBody)) { - $methodBody = $methodBody->getArrayCopy(); - $methodBody['parameters'][] = $acceptLanguageHeader; - - $docs['paths'][$path][$methodName] = $methodBody; - } - } - } - - return $docs; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Swagger/AdminAuthenticationTokenDocumentationNormalizer.php b/src/Sylius/Bundle/ApiBundle/Swagger/AdminAuthenticationTokenDocumentationNormalizer.php deleted file mode 100644 index 65de78a11e..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Swagger/AdminAuthenticationTokenDocumentationNormalizer.php +++ /dev/null @@ -1,97 +0,0 @@ -decoratedNormalizer->supportsNormalization($data, $format); - } - - public function normalize($object, $format = null, array $context = []) - { - $docs = $this->decoratedNormalizer->normalize($object, $format, $context); - - $docs['components']['schemas']['AdminUserToken'] = [ - 'type' => 'object', - 'properties' => [ - 'token' => [ - 'type' => 'string', - 'readOnly' => true, - ], - ], - ]; - - $docs['components']['schemas']['AdminUserCredentials'] = [ - 'type' => 'object', - 'properties' => [ - 'email' => [ - 'type' => 'string', - 'example' => 'api@example.com', - ], - 'password' => [ - 'type' => 'string', - 'example' => 'sylius-api', - ], - ], - ]; - - $tokenDocumentation = [ - 'paths' => [ - $this->apiRoute . '/admin/authentication-token' => [ - 'post' => [ - 'tags' => ['AdminUserToken'], - 'operationId' => 'postCredentialsItem', - 'summary' => 'Get JWT token to login.', - 'requestBody' => [ - 'description' => 'Create new JWT Token', - 'content' => [ - 'application/json' => [ - 'schema' => [ - '$ref' => '#/components/schemas/AdminUserCredentials', - ], - ], - ], - ], - 'responses' => [ - Response::HTTP_OK => [ - 'description' => 'Get JWT token', - 'content' => [ - 'application/json' => [ - 'schema' => [ - '$ref' => '#/components/schemas/AdminUserToken', - ], - ], - ], - ], - ], - ], - ], - ], - ]; - - return array_merge_recursive($tokenDocumentation, $docs); - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Swagger/PathHiderDocumentationNormalizer.php b/src/Sylius/Bundle/ApiBundle/Swagger/PathHiderDocumentationNormalizer.php deleted file mode 100644 index 42b3049eb9..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Swagger/PathHiderDocumentationNormalizer.php +++ /dev/null @@ -1,46 +0,0 @@ -decoratedNormalizer->supportsNormalization($data, $format); - } - - public function normalize($object, $format = null, array $context = []) - { - $docs = $this->decoratedNormalizer->normalize($object, $format, $context); - $paths = (array) $docs['paths']; - - foreach ($this->apiRoutes as $apiRoute) { - if (array_key_exists($apiRoute, $paths)) { - unset($paths[$apiRoute]); - } - } - - $docs['paths'] = $paths; - - return $docs; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Swagger/ProductDocumentationNormalizer.php b/src/Sylius/Bundle/ApiBundle/Swagger/ProductDocumentationNormalizer.php deleted file mode 100644 index 5cb3bf9d8f..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Swagger/ProductDocumentationNormalizer.php +++ /dev/null @@ -1,45 +0,0 @@ -decoratedNormalizer->supportsNormalization($data, $format); - } - - public function normalize($object, $format = null, array $context = []) - { - $docs = $this->decoratedNormalizer->normalize($object, $format, $context); - - $defaultVariantSchema = [ - 'type' => 'string', - 'format' => 'iri-reference', - 'nullable' => true, - 'readOnly' => true, - ]; - - $docs['components']['schemas']['Product.jsonld-shop.product.read']['properties']['defaultVariant'] = $defaultVariantSchema; - - return $docs; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Swagger/ProductImageDocumentationNormalizer.php b/src/Sylius/Bundle/ApiBundle/Swagger/ProductImageDocumentationNormalizer.php deleted file mode 100644 index 5867371cb0..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Swagger/ProductImageDocumentationNormalizer.php +++ /dev/null @@ -1,51 +0,0 @@ -decoratedNormalizer->supportsNormalization($data, $format); - } - - public function normalize($object, $format = null, array $context = []) - { - $docs = $this->decoratedNormalizer->normalize($object, $format, $context); - - $enums = $this->filterProvider->provideShopFilters(); - $enums = array_keys($enums); - - $shopProductImagePath = $this->apiRoute . '/shop/product-images/{id}'; - - foreach ($docs['paths'][$shopProductImagePath]['get']['parameters'] as &$param) { - if ($param['in'] === 'query' && $param['name'] === 'filter') { - $param['schema']['enum'] = $enums; - } - } - - return $docs; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Swagger/ProductSlugDocumentationNormalizer.php b/src/Sylius/Bundle/ApiBundle/Swagger/ProductSlugDocumentationNormalizer.php deleted file mode 100644 index f00cb337ef..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Swagger/ProductSlugDocumentationNormalizer.php +++ /dev/null @@ -1,50 +0,0 @@ -decoratedNormalizer->supportsNormalization($data, $format); - } - - public function normalize($object, $format = null, array $context = []) - { - $docs = $this->decoratedNormalizer->normalize($object, $format, $context); - - $shopProductBySlugPath = $this->apiRoute . '/shop/products-by-slug/{slug}'; - $params = $docs['paths'][$shopProductBySlugPath]['get']['parameters']; - - foreach ($params as $index => $param) { - if ($param['name'] === 'code') { - unset($docs['paths'][$shopProductBySlugPath]['get']['parameters'][$index]); - } - } - - // reset key index after unset - $docs['paths'][$shopProductBySlugPath]['get']['parameters'] = array_values($docs['paths'][$shopProductBySlugPath]['get']['parameters']); - - return $docs; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Swagger/ProductVariantDocumentationNormalizer.php b/src/Sylius/Bundle/ApiBundle/Swagger/ProductVariantDocumentationNormalizer.php deleted file mode 100644 index d36855e4cf..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Swagger/ProductVariantDocumentationNormalizer.php +++ /dev/null @@ -1,53 +0,0 @@ -decoratedNormalizer->supportsNormalization($data, $format); - } - - public function normalize($object, $format = null, array $context = []) - { - $docs = $this->decoratedNormalizer->normalize($object, $format, $context); - - $docs['components']['schemas']['ProductVariant.jsonld-shop.product_variant.read']['properties']['price'] = [ - 'type' => 'integer', - 'readOnly' => true, - 'default' => 0, - ]; - - $docs['components']['schemas']['ProductVariant.jsonld-shop.product_variant.read']['properties']['inStock'] = [ - 'type' => 'boolean', - 'readOnly' => true, - ]; - - $docs['components']['schemas']['ProductVariant.jsonld-shop.product_variant.read']['properties']['originalPrice'] = [ - 'type' => 'integer', - 'readOnly' => true, - 'default' => 0, - ]; - - return $docs; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Swagger/ShippingMethodDocumentationNormalizer.php b/src/Sylius/Bundle/ApiBundle/Swagger/ShippingMethodDocumentationNormalizer.php deleted file mode 100644 index 84f091761b..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Swagger/ShippingMethodDocumentationNormalizer.php +++ /dev/null @@ -1,42 +0,0 @@ -decoratedNormalizer->supportsNormalization($data, $format); - } - - public function normalize($object, $format = null, array $context = []) - { - $docs = $this->decoratedNormalizer->normalize($object, $format, $context); - - $docs['components']['schemas']['ShippingMethod.jsonld-shop.shipping_method.read']['properties']['price'] = [ - 'type' => 'integer', - 'readOnly' => true, - 'default' => 0, - ]; - - return $docs; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Swagger/ShopAuthenticationTokenDocumentationNormalizer.php b/src/Sylius/Bundle/ApiBundle/Swagger/ShopAuthenticationTokenDocumentationNormalizer.php deleted file mode 100644 index 5725c04874..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Swagger/ShopAuthenticationTokenDocumentationNormalizer.php +++ /dev/null @@ -1,101 +0,0 @@ -decoratedNormalizer->supportsNormalization($data, $format); - } - - public function normalize($object, $format = null, array $context = []) - { - $docs = $this->decoratedNormalizer->normalize($object, $format, $context); - - $docs['components']['schemas']['ShopUserToken'] = [ - 'type' => 'object', - 'properties' => [ - 'token' => [ - 'type' => 'string', - 'readOnly' => true, - ], - 'customer' => [ - 'type' => 'string', - 'readOnly' => true, - ], - ], - ]; - - $docs['components']['schemas']['ShopUserCredentials'] = [ - 'type' => 'object', - 'properties' => [ - 'email' => [ - 'type' => 'string', - 'example' => 'shop@example.com', - ], - 'password' => [ - 'type' => 'string', - 'example' => 'sylius', - ], - ], - ]; - - $tokenDocumentation = [ - 'paths' => [ - $this->apiRoute . '/shop/authentication-token' => [ - 'post' => [ - 'tags' => ['ShopUserToken'], - 'operationId' => 'postCredentialsItem', - 'summary' => 'Get JWT token to login.', - 'requestBody' => [ - 'description' => 'Create new JWT Token', - 'content' => [ - 'application/json' => [ - 'schema' => [ - '$ref' => '#/components/schemas/ShopUserCredentials', - ], - ], - ], - ], - 'responses' => [ - Response::HTTP_OK => [ - 'description' => 'Get JWT token', - 'content' => [ - 'application/json' => [ - 'schema' => [ - '$ref' => '#/components/schemas/ShopUserToken', - ], - ], - ], - ], - ], - ], - ], - ], - ]; - - return array_merge_recursive($docs, $tokenDocumentation); - } -} diff --git a/src/Sylius/Bundle/ApiBundle/SyliusApiBundle.php b/src/Sylius/Bundle/ApiBundle/SyliusApiBundle.php index 208f52057c..6792edeba4 100644 --- a/src/Sylius/Bundle/ApiBundle/SyliusApiBundle.php +++ b/src/Sylius/Bundle/ApiBundle/SyliusApiBundle.php @@ -14,12 +14,13 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle; use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\CommandDataTransformerPass; +use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\ExtractorMergingCompilerPass; use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\FlattenExceptionNormalizerDecoratorCompilerPass; use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\LegacyErrorHandlingCompilerPass; +use Sylius\Bundle\ApiBundle\DependencyInjection\Compiler\SyliusPriceHistoryLegacyAliasesPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; -/** @experimental */ final class SyliusApiBundle extends Bundle { public function build(ContainerBuilder $container): void @@ -27,5 +28,7 @@ final class SyliusApiBundle extends Bundle $container->addCompilerPass(new CommandDataTransformerPass()); $container->addCompilerPass(new FlattenExceptionNormalizerDecoratorCompilerPass()); $container->addCompilerPass(new LegacyErrorHandlingCompilerPass()); + $container->addCompilerPass(new SyliusPriceHistoryLegacyAliasesPass()); + $container->addCompilerPass(new ExtractorMergingCompilerPass()); } } diff --git a/src/Sylius/Bundle/ApiBundle/Tests/ApiPlatform/Bridge/Symfony/Routing/CachedRouteNameResolverTest.php b/src/Sylius/Bundle/ApiBundle/Tests/ApiPlatform/Bridge/Symfony/Routing/CachedRouteNameResolverTest.php index 5c5affbefd..00ba8721a0 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/ApiPlatform/Bridge/Symfony/Routing/CachedRouteNameResolverTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/ApiPlatform/Bridge/Symfony/Routing/CachedRouteNameResolverTest.php @@ -18,6 +18,7 @@ use ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameResolverInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; use Sylius\Bundle\ApiBundle\ApiPlatform\Bridge\Symfony\Routing\CachedRouteNameResolver; @@ -26,6 +27,8 @@ use Symfony\Component\Cache\Exception\CacheException; final class CachedRouteNameResolverTest extends TestCase { + use ProphecyTrait; + /** * @test */ diff --git a/src/Sylius/Bundle/ApiBundle/Tests/ApiPlatform/Bridge/Symfony/Routing/RouteNameResolverTest.php b/src/Sylius/Bundle/ApiBundle/Tests/ApiPlatform/Bridge/Symfony/Routing/RouteNameResolverTest.php index 220176d27f..f6af06e3c3 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/ApiPlatform/Bridge/Symfony/Routing/RouteNameResolverTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/ApiPlatform/Bridge/Symfony/Routing/RouteNameResolverTest.php @@ -15,6 +15,7 @@ namespace Sylius\Bundle\ApiBundle\Tests\ApiPlatform\Bridge\Symfony\Routing; use ApiPlatform\Core\Api\OperationType; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; use Sylius\Bundle\ApiBundle\ApiPlatform\Bridge\Symfony\Routing\RouteNameResolver; use Sylius\Bundle\ApiBundle\Provider\PathPrefixProviderInterface; use Symfony\Component\Routing\Route; @@ -23,6 +24,8 @@ use Symfony\Component\Routing\RouterInterface; final class RouteNameResolverTest extends TestCase { + use ProphecyTrait; + /** * @test */ diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/_sylius.yaml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/_sylius.yaml new file mode 100644 index 0000000000..864d8e9fe0 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/_sylius.yaml @@ -0,0 +1,2 @@ +sylius_user: + encoder: plaintext diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Country.xml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Country.xml new file mode 100644 index 0000000000..ca75d2ddb7 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Country.xml @@ -0,0 +1,71 @@ + + + + + + + + + GET + /admin/updated/countries + + test.country.id_filter + + + + + false + + + + + + GET + /shop/countries/new/{code} + + + shop:country:show + sylius:shop:country:show + + + + + + false + + + + DELETE + /admin/countries/{code} + + + + + + false + + + + DELETE + /admin/countries/{code}/provinces/{id} + + + + + + + + + diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Foo.xml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Foo.xml index 656daa446b..37911e4282 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Foo.xml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Foo.xml @@ -16,14 +16,20 @@ GET - foo:read + + foo:read + sylius:foo:read + POST - foo:create + + foo:create + sylius:foo:create + @@ -34,7 +40,10 @@ Sylius\Bundle\ApiBundle\Application\Command\FooCommand false - foo:read + + foo:read + sylius:foo:read + @@ -43,7 +52,10 @@ GET - foo:read + + foo:read + sylius:foo:read + @@ -51,7 +63,10 @@ GET /admin/foos/{id} - foo:read + + foo:read + sylius:foo:read + diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/FooSyliusResource.xml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/FooSyliusResource.xml index 3a36b46a45..452f27734c 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/FooSyliusResource.xml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/FooSyliusResource.xml @@ -16,14 +16,20 @@ GET - foo-sylius-resource:read + + foo-sylius-resource:read + sylius:foo-sylius-resource:read + POST - foo-sylius-resource:create + + foo-sylius-resource:create + sylius:foo-sylius-resource:create + @@ -32,7 +38,10 @@ GET - foo-sylius-resource:read + + foo-sylius-resource:read + sylius:foo-sylius-resource:read + diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Promotion.xml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Promotion.xml index de34242708..ccb0a0fc5b 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Promotion.xml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Promotion.xml @@ -24,7 +24,10 @@ GET - admin:promotion:read + + admin:promotion:index + sylius:admin:promotion:index + @@ -33,7 +36,10 @@ GET - admin:promotion:read + + admin:promotion:show + sylius:admin:promotion:show + diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Taxon.xml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Taxon.xml index befed260da..cbf605ab72 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Taxon.xml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/Taxon.xml @@ -12,7 +12,10 @@ GET /admin/taxons - admin:taxon:read + + admin:taxon:index + sylius:admin:taxon:index + @@ -20,7 +23,10 @@ POST /admin/taxons - admin:taxon:create + + admin:taxon:create + sylius:admin:taxon:create + @@ -28,7 +34,10 @@ GET /shop/taxons - shop:taxon:read + + shop:taxon:index + sylius:shop:taxon:index + @@ -38,7 +47,10 @@ GET /admin/taxons/{code} - admin:taxon:read + + admin:taxon:show + sylius:admin:taxon:show + @@ -46,7 +58,10 @@ PUT /admin/taxons/{code} - admin:taxon:update + + admin:taxon:update + sylius:admin:taxon:update + @@ -54,7 +69,10 @@ GET /shop/taxons/{code} - shop:taxon:read + + shop:taxon:show + sylius:shop:taxon:show + diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/config.yaml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/config.yaml index 96b2cab18b..641cee55fd 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/config.yaml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/api_platform/config.yaml @@ -11,7 +11,7 @@ method: GET path: /shop/channels-new-path normalization_context: - groups: ['shop:channel:read'] + groups: ['shop:channel:index'] filters: ['test.channel.id_filter'] '%sylius.model.order.class%': @@ -29,7 +29,7 @@ admin_get: method: GET normalization_context: - groups: ['admin:promotion:read'] + groups: ['admin:promotion:index'] admin_post: method: GET denormalization_context: @@ -38,6 +38,6 @@ admin_get: method: GET normalization_context: - groups: ['admin:promotion:read'] + groups: ['admin:promotion:show'] admin_delete: method: DELETE diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/bundles.php b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/bundles.php index f1ad14ebee..a96c7e4206 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/bundles.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/bundles.php @@ -18,6 +18,7 @@ return [ Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Sylius\Calendar\SyliusCalendarBundle::class => ['all' => true], + Sylius\Abstraction\StateMachine\SyliusStateMachineAbstractionBundle::class => ['all' => true], Sylius\Bundle\OrderBundle\SyliusOrderBundle::class => ['all' => true], Sylius\Bundle\MoneyBundle\SyliusMoneyBundle::class => ['all' => true], Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle::class => ['all' => true], diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/config.yaml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/config.yaml index 8df31e8d85..016b52f50f 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/config.yaml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/config.yaml @@ -1,6 +1,7 @@ imports: - { resource: "@SyliusCoreBundle/Resources/config/app/config.yml" } - { resource: "@SyliusApiBundle/Resources/config/app/config.yaml" } + - { resource: "_sylius.yaml" } - { resource: "security.yaml" } parameters: @@ -38,6 +39,7 @@ framework: paths: ['%kernel.project_dir%/config/serialization'] mailer: dsn: 'null://null' + workflows: ~ doctrine_migrations: storage: @@ -84,6 +86,11 @@ services: arguments: [{id: 'exact', hostname: 'partial'}] tags: ['api_platform.filter'] + test.country.id_filter: + parent: api_platform.doctrine.orm.search_filter + arguments: [{id: 'exact', code: 'partial'}] + tags: ['api_platform.filter'] + Sylius\Bundle\ApiBundle\Application\CommandHandler\FooHandler: tags: - { name: messenger.message_handler, bus: sylius.command_bus } diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/fixtures/channel.yaml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/fixtures/channel.yaml index 61771260b1..f165cc1174 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/fixtures/channel.yaml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/fixtures/channel.yaml @@ -18,3 +18,7 @@ Sylius\Component\Currency\Model\Currency: Sylius\Component\Locale\Model\Locale: locale: code: "en_US" + +Sylius\Component\Addressing\Model\Country: + country_US: + code: "US" diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/security.yaml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/security.yaml index 133c54f239..c63b42cde9 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/security.yaml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/security.yaml @@ -7,8 +7,7 @@ security: id: sylius.shop_user_provider.email_or_name_based password_hashers: - sha512: sha512 - Sylius\Component\User\Model\UserInterface: sha512 + Sylius\Component\User\Model\UserInterface: plaintext firewalls: new_api_admin_user: @@ -45,6 +44,3 @@ security: - { path: "%sylius.security.new_api_user_account_regex%/.*", role: ROLE_USER } - { path: "%sylius.security.new_api_shop_route%/authentication-token", role: PUBLIC_ACCESS } - { path: "%sylius.security.new_api_shop_regex%/.*", role: PUBLIC_ACCESS } - -sylius_user: - encoder: sha512 diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/Foo.xml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/Foo.xml index 4cee80ea78..bbcf47df2e 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/Foo.xml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/Foo.xml @@ -18,18 +18,25 @@ foo:read + sylius:foo:read foo:read + sylius:foo:read foo:create + sylius:foo:create foo:read + sylius:foo:read foo:create + sylius:foo:create foo:read + sylius:foo:read foo:create + sylius:foo:create diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/FooSyliusResource.xml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/FooSyliusResource.xml index 53b80e79a1..c96a24d911 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/FooSyliusResource.xml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/FooSyliusResource.xml @@ -18,10 +18,13 @@ foo-sylius-resource:read + sylius:foo-sylius-resource:read foo-sylius-resource:read + sylius:foo-sylius-resource:read foo-sylius-resource:create + sylius:foo-sylius-resource:create diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/Taxon.xml b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/Taxon.xml index 5bd2806ae7..e7bafcb33c 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/Taxon.xml +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/config/serialization/Taxon.xml @@ -6,22 +6,48 @@ > - admin:taxon:read - shop:taxon:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + shop:taxon:index + sylius:shop:taxon:index + shop:taxon:show + sylius:shop:taxon:show - admin:taxon:read - shop:taxon:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + shop:taxon:index + sylius:shop:taxon:index + shop:taxon:show + sylius:shop:taxon:show admin:taxon:create + sylius:admin:taxon:create admin:taxon:update + sylius:admin:taxon:update - admin:taxon:read - shop:taxon:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + shop:taxon:index + sylius:shop:taxon:index + shop:taxon:show + sylius:shop:taxon:show - admin:taxon:read - shop:taxon:read + admin:taxon:index + sylius:admin:taxon:index + admin:taxon:show + sylius:admin:taxon:show + shop:taxon:index + sylius:shop:taxon:index + shop:taxon:show + sylius:shop:taxon:show diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/DisablingApiTest.php b/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/DisablingApiTest.php index 99a4968e4f..8839406210 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/DisablingApiTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/DisablingApiTest.php @@ -17,7 +17,7 @@ use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -class DisablingApiTest extends ApiTestCase +final class DisablingApiTest extends ApiTestCase { use SetUpTestsTrait; diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/DisablingDocumentationTest.php b/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/DisablingDocumentationTest.php index c16189181c..a50e7c8640 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/DisablingDocumentationTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/DisablingDocumentationTest.php @@ -16,7 +16,7 @@ namespace Sylius\Bundle\ApiBundle\Application\Tests; use ApiPlatform\Core\Bridge\Symfony\Bundle\Test\ApiTestCase; use Symfony\Component\HttpFoundation\Response; -class DisablingDocumentationTest extends ApiTestCase +final class DisablingDocumentationTest extends ApiTestCase { use SetUpTestsTrait; diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/SyliusConfigMergeTest.php b/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/SyliusConfigMergeTest.php index 01c1768b96..1b9f3f0896 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/SyliusConfigMergeTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/Application/src/Tests/SyliusConfigMergeTest.php @@ -27,10 +27,8 @@ final class SyliusConfigMergeTest extends ApiTestCase $this->setUpTest(); } - /** - * @test - */ - public function it_removes_api_method_to_endpoint(): void + /** @test */ + public function it_removes_api_method_to_endpoint_with_yaml(): void { static::createClient()->request( 'GET', @@ -38,60 +36,111 @@ final class SyliusConfigMergeTest extends ApiTestCase ['auth_bearer' => $this->JWTAdminUserToken], ); - $this->assertResponseStatusCodeSame(404); + self::assertResponseStatusCodeSame(404); } - /** - * @test - */ - public function it_allows_to_add_new_operation(): void + /** @test */ + public function it_removes_api_method_to_endpoint_with_xml(): void + { + static::createClient()->request( + 'GET', + '/api/v2/shop/countries', + ); + + self::assertResponseStatusCodeSame(404); + } + + /** @test */ + public function it_allows_to_add_new_operation_with_yaml(): void { static::createClient()->request( 'GET', '/api/v2/shop/channels-new-path', ); - $this->assertResponseIsSuccessful(); - $this->assertJsonContains(['@type' => 'hydra:Collection']); + self::assertResponseIsSuccessful(); + self::assertJsonContains(['@type' => 'hydra:Collection']); } - /** - * @test - */ - public function it_allows_to_add_new_filter(): void + /** @test */ + public function it_allows_to_add_new_operation_with_xml(): void + { + static::createClient()->request( + 'DELETE', + '/api/v2/admin/countries/US', + ['auth_bearer' => $this->JWTAdminUserToken], + ); + + self::assertResponseIsSuccessful(); + } + + /** @test */ + public function it_allows_to_add_new_filter_with_yaml(): void { static::createClient()->request( 'GET', '/api/v2/shop/channels-new-path?id=20', ); - $this->assertJsonContains(['hydra:totalItems' => 0]); + self::assertResponseIsSuccessful(); + self::assertJsonContains(['hydra:totalItems' => 0]); static::createClient()->request( 'GET', '/api/v2/shop/channels-new-path', ); - $this->assertJsonContains(['hydra:totalItems' => 1]); + self::assertResponseIsSuccessful(); + self::assertJsonContains(['hydra:totalItems' => 1]); } - /** - * @test - */ - public function it_merges_configs(): void + /** @test */ + public function it_allows_to_add_new_filter_with_xml(): void + { + static::createClient()->request( + 'GET', + '/api/v2/admin/updated/countries?id=42', + ['auth_bearer' => $this->JWTAdminUserToken], + ); + + self::assertResponseIsSuccessful(); + self::assertJsonContains(['hydra:totalItems' => 0]); + + static::createClient()->request( + 'GET', + '/api/v2/admin/updated/countries', + ['auth_bearer' => $this->JWTAdminUserToken], + ); + + self::assertResponseIsSuccessful(); + self::assertJsonContains(['hydra:totalItems' => 1]); + } + + /** @test */ + public function it_merges_configs_with_yaml(): void { static::createClient()->request( 'GET', '/api/v2/shop/channels/WEB', ); - $this->assertResponseIsSuccessful(); + self::assertResponseIsSuccessful(); } - /** - * @test - */ - public function it_allows_to_overwrite_endpoint(): void + /** @test */ + public function it_merges_configs_with_xml(): void + { + static::createClient()->request( + 'GET', + '/api/v2/admin/countries', + ['auth_bearer' => $this->JWTAdminUserToken], + ); + + self::assertResponseIsSuccessful(); + } + + /** @test */ + public function it_allows_to_overwrite_endpoint_with_yaml(): void { static::createClient()->request( 'GET', @@ -99,7 +148,7 @@ final class SyliusConfigMergeTest extends ApiTestCase ['auth_bearer' => $this->JWTAdminUserToken], ); - $this->assertResponseStatusCodeSame(404); + self::assertResponseStatusCodeSame(404); static::createClient()->request( 'GET', @@ -107,25 +156,42 @@ final class SyliusConfigMergeTest extends ApiTestCase ['auth_bearer' => $this->JWTAdminUserToken], ); - $this->assertResponseIsSuccessful(); + self::assertResponseIsSuccessful(); } - /** - * @test - */ - public function it_allows_to_remove_non_crud_endpoint(): void + /** @test */ + public function it_allows_to_overwrite_endpoint_with_xml(): void { - $response = - json_decode( - static::createClient() - ->request( - 'PATCH', - '/api/v2/shop/orders/TOKEN/shipments/TEST', - )->getContent(false), - true, - ); + static::createClient()->request( + 'GET', + '/api/v2/shop/countries/US', + ); - $this->assertResponseStatusCodeSame(404); + self::assertResponseStatusCodeSame(404); + + static::createClient()->request( + 'GET', + '/api/v2/shop/countries/new/US', + ); + + self::assertResponseIsSuccessful(); + } + + /** @test */ + public function it_allows_to_remove_non_crud_endpoint_with_yaml(): void + { + $response = json_decode( + static::createClient() + ->request( + 'PATCH', + '/api/v2/shop/orders/TOKEN/shipments/TEST', + )->getContent(false), + true, + 512, + \JSON_THROW_ON_ERROR, + ); + + self::assertResponseStatusCodeSame(404); Assert::contains($response['hydra:description'], 'No route found'); } } diff --git a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountRegistrationEmailHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountRegistrationEmailHandlerTest.php deleted file mode 100644 index 12fa4433c5..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountRegistrationEmailHandlerTest.php +++ /dev/null @@ -1,144 +0,0 @@ -isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - - $container = self::getContainer(); - - /** @var TranslatorInterface $translator */ - $translator = $container->get('translator'); - - $emailSender = $container->get('sylius.email_sender'); - - /** @var ChannelRepositoryInterface|ObjectProphecy $channelRepository */ - $channelRepository = $this->prophesize(ChannelRepositoryInterface::class); - /** @var UserRepositoryInterface|ObjectProphecy $userRepository */ - $userRepository = $this->prophesize(UserRepositoryInterface::class); - /** @var ChannelInterface|ObjectProphecy $channel */ - $channel = $this->prophesize(ChannelInterface::class); - /** @var UserInterface|ObjectProphecy $user */ - $user = $this->prophesize(UserInterface::class); - - $user->getUsername()->willReturn('username'); - $user->getEmailVerificationToken()->willReturn('token'); - - $channelRepository->findOneByCode('CHANNEL_CODE')->willReturn($channel->reveal()); - $userRepository->findOneByEmail('user@example.com')->willReturn($user->reveal()); - - $sendAccountRegistrationEmailHandler = new SendAccountRegistrationEmailHandler( - $userRepository->reveal(), - $channelRepository->reveal(), - $emailSender, - ); - - $sendAccountRegistrationEmailHandler( - new SendAccountRegistrationEmail( - 'user@example.com', - 'en_US', - 'CHANNEL_CODE', - ), - ); - - self::assertEmailCount(1); - $email = self::getMailerMessage(); - self::assertEmailAddressContains($email, 'To', 'user@example.com'); - self::assertEmailHtmlBodyContains($email, $translator->trans('sylius.email.user_registration.start_shopping', [], null, 'en_US')); - } - - /** @test */ - public function it_sends_account_registration_email_with_swiftmailer(): void - { - if (!$this->isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment with swiftmailer'); - } - - $container = self::getContainer(); - - self::setSpoolDirectory($container->getParameter('kernel.cache_dir') . '/spool'); - - /** @var Filesystem $filesystem */ - $filesystem = $container->get('test.filesystem.public'); - - /** @var TranslatorInterface $translator */ - $translator = $container->get('translator'); - - $filesystem->remove(self::getSpoolDirectory()); - - $emailSender = $container->get('sylius.email_sender'); - - /** @var ChannelRepositoryInterface|ObjectProphecy $channelRepository */ - $channelRepository = $this->prophesize(ChannelRepositoryInterface::class); - /** @var UserRepositoryInterface|ObjectProphecy $userRepository */ - $userRepository = $this->prophesize(UserRepositoryInterface::class); - /** @var ChannelInterface|ObjectProphecy $channel */ - $channel = $this->prophesize(ChannelInterface::class); - /** @var UserInterface|ObjectProphecy $user */ - $user = $this->prophesize(UserInterface::class); - - $user->getUsername()->willReturn('username'); - $user->getEmailVerificationToken()->willReturn('token'); - - $channelRepository->findOneByCode('CHANNEL_CODE')->willReturn($channel->reveal()); - $userRepository->findOneByEmail('user@example.com')->willReturn($user->reveal()); - - $sendAccountRegistrationEmailHandler = new SendAccountRegistrationEmailHandler( - $userRepository->reveal(), - $channelRepository->reveal(), - $emailSender, - ); - - $sendAccountRegistrationEmailHandler( - new SendAccountRegistrationEmail( - 'user@example.com', - 'en_US', - 'CHANNEL_CODE', - ), - ); - - self::assertSpooledMessagesCountWithRecipient(1, 'user@example.com'); - self::assertSpooledMessageWithContentHasRecipient( - $translator->trans('sylius.email.user_registration.start_shopping', [], null, 'en_US'), - 'user@example.com', - ); - } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountVerificationEmailHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountVerificationEmailHandlerTest.php deleted file mode 100644 index dc0a29bac8..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendAccountVerificationEmailHandlerTest.php +++ /dev/null @@ -1,89 +0,0 @@ -isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - - $container = self::getContainer(); - - /** @var TranslatorInterface $translator */ - $translator = $container->get('translator'); - - $emailSender = $container->get('sylius.email_sender'); - - /** @var ChannelRepositoryInterface|ObjectProphecy $channelRepository */ - $channelRepository = $this->prophesize(ChannelRepositoryInterface::class); - /** @var UserRepositoryInterface|ObjectProphecy $userRepository */ - $userRepository = $this->prophesize(UserRepositoryInterface::class); - /** @var ChannelInterface|ObjectProphecy $channel */ - $channel = $this->prophesize(ChannelInterface::class); - /** @var UserInterface|ObjectProphecy $user */ - $user = $this->prophesize(UserInterface::class); - - $user->getUsername()->willReturn('username'); - $user->getEmailVerificationToken()->willReturn('token'); - - $channelRepository->findOneByCode('CHANNEL_CODE')->willReturn($channel->reveal()); - $userRepository->findOneByEmail('user@example.com')->willReturn($user->reveal()); - - $sendAccountVerificationEmailHandler = new SendAccountVerificationEmailHandler( - $userRepository->reveal(), - $channelRepository->reveal(), - $emailSender, - ); - - $sendAccountVerificationEmailHandler( - new SendAccountVerificationEmail( - 'user@example.com', - 'en_US', - 'CHANNEL_CODE', - ), - ); - - self::assertEmailCount(1); - $email = self::getMailerMessage(); - self::assertEmailAddressContains($email, 'To', 'user@example.com'); - self::assertEmailHtmlBodyContains( - $email, - $translator->trans('sylius.email.verification_token.message', [], null, 'en_US'), - ); - } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendOrderConfirmationEmailHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendOrderConfirmationEmailHandlerTest.php deleted file mode 100644 index 0196e7e5d2..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendOrderConfirmationEmailHandlerTest.php +++ /dev/null @@ -1,78 +0,0 @@ -isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - - $container = self::bootKernel()->getContainer(); - $emailSender = $container->get('sylius.email_sender'); - - /** @var OrderInterface|ObjectProphecy $order */ - $order = $this->prophesize(OrderInterface::class); - /** @var CustomerInterface|ObjectProphecy $customer */ - $customer = $this->prophesize(CustomerInterface::class); - $customer->getEmail()->willReturn('johnny.bravo@email.com'); - /** @var ChannelInterface|ObjectProphecy $channel */ - $channel = $this->prophesize(ChannelInterface::class); - - $order->getCustomer()->willReturn($customer->reveal()); - $order->getChannel()->willReturn($channel->reveal()); - $order->getLocaleCode()->willReturn('pl_PL'); - $order->getNumber()->willReturn('#000001'); - $order->getTokenValue()->willReturn('TOKEN'); - - /** @var OrderRepositoryInterface $orderRepository */ - $orderRepository = $this->prophesize(OrderRepositoryInterface::class); - - $orderRepository->findOneByTokenValue('TOKEN')->willReturn($order); - - $sendOrderConfirmationEmailHandler = new SendOrderConfirmationHandler( - $emailSender, - $orderRepository->reveal(), - ); - - $sendOrderConfirmationEmailHandler(new SendOrderConfirmation('TOKEN')); - - $this->assertEmailCount(1); - $email = $this->getMailerMessage(); - $this->assertEmailAddressContains($email, 'To', 'johnny.bravo@email.com'); - $this->assertEmailHtmlBodyContains($email, '#000001'); - } - - private function isItSwiftmailerTestEnv(): bool - { - $env = $this->getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendResetPasswordEmailHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendResetPasswordEmailHandlerTest.php deleted file mode 100644 index 86e92c768a..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendResetPasswordEmailHandlerTest.php +++ /dev/null @@ -1,87 +0,0 @@ -isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - - $container = self::getContainer(); - - /** @var TranslatorInterface $translator */ - $translator = $container->get('translator'); - - $emailSender = $container->get('sylius.email_sender'); - - /** @var ChannelRepositoryInterface|ObjectProphecy $channelRepository */ - $channelRepository = $this->prophesize(ChannelRepositoryInterface::class); - /** @var UserRepositoryInterface|ObjectProphecy $userRepository */ - $userRepository = $this->prophesize(UserRepositoryInterface::class); - /** @var ChannelInterface|ObjectProphecy $channel */ - $channel = $this->prophesize(ChannelInterface::class); - /** @var UserInterface|ObjectProphecy $user */ - $user = $this->prophesize(UserInterface::class); - - $user->getUsername()->willReturn('username'); - $user->getPasswordResetToken()->willReturn('token'); - - $channelRepository->findOneByCode('CHANNEL_CODE')->willReturn($channel->reveal()); - $userRepository->findOneByEmail('user@example.com')->willReturn($user->reveal()); - - $resetPasswordEmailHandler = new SendResetPasswordEmailHandler( - $emailSender, - $channelRepository->reveal(), - $userRepository->reveal(), - ); - - $resetPasswordEmailHandler(new SendResetPasswordEmail( - 'user@example.com', - 'CHANNEL_CODE', - 'en_US', - )); - - self::assertEmailCount(1); - $email = self::getMailerMessage(); - self::assertEmailAddressContains($email, 'To', 'user@example.com'); - self::assertEmailHtmlBodyContains( - $email, - $translator->trans('sylius.email.password_reset.to_reset_your_password_token', [], null, 'en_US'), - ); - } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Converter/IriToIdentifierConverterTest.php b/src/Sylius/Bundle/ApiBundle/Tests/Converter/IriToIdentifierConverterTest.php index d8e31ff7c0..d32c17236a 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Converter/IriToIdentifierConverterTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/Converter/IriToIdentifierConverterTest.php @@ -17,6 +17,7 @@ use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Identifier\IdentifierConverterInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Sylius\Bundle\ApiBundle\Command\Catalog\AddProductReview; use Sylius\Bundle\ApiBundle\Converter\IriToIdentifierConverter; @@ -26,7 +27,9 @@ use Symfony\Component\Routing\RouterInterface; final class IriToIdentifierConverterTest extends TestCase { - private RouterInterface|ObjectProphecy $router; + use ProphecyTrait; + + private ObjectProphecy|RouterInterface $router; private IdentifierConverterInterface|ObjectProphecy $identifierConverter; diff --git a/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/Compiler/ExtractorMergingCompilerPassTest.php b/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/Compiler/ExtractorMergingCompilerPassTest.php new file mode 100644 index 0000000000..9b3d39e18f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/Compiler/ExtractorMergingCompilerPassTest.php @@ -0,0 +1,60 @@ +compile(); + + $this->assertContainerBuilderNotHasService('api_platform.metadata.extractor.xml.legacy'); + } + + /** @test */ + public function it_overwrites_xml_extractor(): void + { + $this->setDefinition( + 'api_platform.metadata.extractor.xml.legacy', + new Definition(null, [[], new Reference('service_container')]), + ); + $this->setDefinition(LegacyResourceMetadataMerger::class, new Definition()); + + $this->compile(); + + $this->assertContainerBuilderHasService( + 'api_platform.metadata.extractor.xml.legacy', + MergingXmlExtractor::class, + ); + $this->assertContainerBuilderHasServiceDefinitionWithArgument( + 'api_platform.metadata.extractor.xml.legacy', + 2, + new Reference(LegacyResourceMetadataMerger::class), + ); + } + + protected function registerCompilerPass(ContainerBuilder $container): void + { + $container->addCompilerPass(new ExtractorMergingCompilerPass()); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPassTest.php b/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPassTest.php new file mode 100644 index 0000000000..1ca92d0469 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPassTest.php @@ -0,0 +1,43 @@ +register('Sylius\Bundle\ApiBundle\Serializer\ChannelDenormalizer'); + $container->register('Sylius\Bundle\ApiBundle\Validator\ResourceApiInputDataPropertiesValidatorInterface'); + + $this->process($container); + + $this->assertHasAlias($container, 'Sylius\PriceHistoryPlugin\Infrastructure\Serializer\ChannelDenormalizer'); + $this->assertHasAlias($container, 'Sylius\PriceHistoryPlugin\Application\Validator\ResourceInputDataPropertiesValidatorInterface'); + } + + private function assertHasAlias(ContainerBuilder $container, string $alias): void + { + $this->assertTrue($container->hasAlias($alias), 'Expected to find alias ' . $alias); + } + + private function process(ContainerBuilder $container): void + { + (new SyliusPriceHistoryLegacyAliasesPass())->process($container); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/SyliusApiExtensionTest.php b/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/SyliusApiExtensionTest.php index 2eb6580611..da649561b4 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/SyliusApiExtensionTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/DependencyInjection/SyliusApiExtensionTest.php @@ -14,8 +14,15 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Tests\DependencyInjection; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; -use Sylius\Bundle\ApiBundle\ApiPlatform\Bridge\Symfony\Bundle\Action\SwaggerUiAction; +use Sylius\Bundle\ApiBundle\Attribute\AsCommandDataTransformer; +use Sylius\Bundle\ApiBundle\Attribute\AsDocumentationModifier; +use Sylius\Bundle\ApiBundle\Attribute\AsPaymentConfigurationProvider; use Sylius\Bundle\ApiBundle\DependencyInjection\SyliusApiExtension; +use Sylius\Bundle\ApiBundle\Tests\Stub\CommandDataTransformerStub; +use Sylius\Bundle\ApiBundle\Tests\Stub\DocumentationModifierStub; +use Sylius\Bundle\ApiBundle\Tests\Stub\PaymentConfigurationProviderStub; +use Sylius\Component\Core\Model\OrderInterface; +use Symfony\Component\DependencyInjection\Definition; final class SyliusApiExtensionTest extends AbstractExtensionTestCase { @@ -24,10 +31,10 @@ final class SyliusApiExtensionTest extends AbstractExtensionTestCase { $this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]); - $this->setParameter('api_platform.enable_swagger_ui', true); + $this->setParameter('api_platform.swagger.api_keys', []); $this->load(); - $this->assertContainerBuilderHasService('api_platform.swagger.action.ui', SwaggerUiAction::class); + $this->assertContainerBuilderHasService('Sylius\Bundle\ApiBundle\OpenApi\Documentation\AcceptLanguageHeaderDocumentationModifier'); } /** @test */ @@ -35,20 +42,9 @@ final class SyliusApiExtensionTest extends AbstractExtensionTestCase { $this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]); - $this->setParameter('api_platform.enable_swagger_ui', false); $this->load(); - $this->assertContainerBuilderNotHasService('api_platform.swagger.action.ui'); - } - - /** @test */ - public function it_does_not_load_swagger_integration_if_it_does_not_exists(): void - { - $this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]); - - $this->load(); - - $this->assertContainerBuilderNotHasService('api_platform.swagger.action.ui'); + $this->assertContainerBuilderNotHasService('Sylius\Bundle\ApiBundle\OpenApi\Documentation\AcceptLanguageHeaderDocumentationModifier'); } /** @test */ @@ -92,6 +88,46 @@ final class SyliusApiExtensionTest extends AbstractExtensionTestCase ); } + /** @test */ + public function it_loads_order_states_to_filter_out_parameter_properly(): void + { + $this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]); + + $this->load([ + 'order_states_to_filter_out' => [ + OrderInterface::STATE_CART, + OrderInterface::STATE_NEW, + ], + ]); + + $this->assertContainerBuilderHasParameter( + 'sylius_api.order_states_to_filter_out', + [OrderInterface::STATE_CART, OrderInterface::STATE_NEW], + ); + } + + /** @test */ + public function it_loads_skip_read_and_skip_index_and_show_serialization_groups_parameters_properly(): void + { + $this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]); + + $this->load([ + 'serialization_groups' => [ + 'skip_adding_read_group' => true, + 'skip_adding_index_and_show_groups' => false, + ], + ]); + + $this->assertContainerBuilderHasParameter( + 'sylius_api.serialization_groups.skip_adding_read_group', + true, + ); + $this->assertContainerBuilderHasParameter( + 'sylius_api.serialization_groups.skip_adding_index_and_show_groups', + false, + ); + } + /** @test */ public function it_loads_default_filter_eager_loading_extension_restricted_operations_configuration_properly(): void { @@ -116,6 +152,69 @@ final class SyliusApiExtensionTest extends AbstractExtensionTestCase ]); } + /** @test */ + public function it_autoconfigures_command_data_transformer_with_attribute(): void + { + $this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]); + $this->container->setDefinition( + 'acme.command_data_transformer_with_attribute', + (new Definition()) + ->setClass(CommandDataTransformerStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.command_data_transformer_with_attribute', + AsCommandDataTransformer::SERVICE_TAG, + ['priority' => 15], + ); + } + + /** @test */ + public function it_autoconfigures_documentation_modifier_with_attribute(): void + { + $this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]); + $this->container->setDefinition( + 'acme.documentation_modifier_with_attribute', + (new Definition()) + ->setClass(DocumentationModifierStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.documentation_modifier_with_attribute', + AsDocumentationModifier::SERVICE_TAG, + ['priority' => 15], + ); + } + + /** @test */ + public function it_autoconfigures_payment_configuration_provider_with_attribute(): void + { + $this->container->setParameter('kernel.bundles_metadata', ['SyliusApiBundle' => ['path' => __DIR__ . '../..']]); + $this->container->setDefinition( + 'acme.payment_configuration_provider_with_attribute', + (new Definition()) + ->setClass(PaymentConfigurationProviderStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.payment_configuration_provider_with_attribute', + AsPaymentConfigurationProvider::SERVICE_TAG, + ['priority' => 5], + ); + } + protected function getContainerExtensions(): array { return [ diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Listener/PostgreSQLDriverExceptionListenerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/Listener/PostgreSQLDriverExceptionListenerTest.php index 1508531d4f..264ff29b9c 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/Listener/PostgreSQLDriverExceptionListenerTest.php +++ b/src/Sylius/Bundle/ApiBundle/Tests/Listener/PostgreSQLDriverExceptionListenerTest.php @@ -21,7 +21,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; -class PostgreSQLDriverExceptionListenerTest extends TestCase +final class PostgreSQLDriverExceptionListenerTest extends TestCase { /** @test */ public function it_does_nothing_if_exception_is_not_a_driver_exception(): void diff --git a/src/Sylius/Bundle/ApiBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php b/src/Sylius/Bundle/ApiBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php deleted file mode 100644 index be39846a29..0000000000 --- a/src/Sylius/Bundle/ApiBundle/Tests/MessageHandler/Admin/SendResetPasswordEmailHandlerTest.php +++ /dev/null @@ -1,76 +0,0 @@ -isItSwiftmailerTestEnv()) { - $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); - } - - $container = self::getContainer(); - - /** @var TranslatorInterface $translator */ - $translator = $container->get('translator'); - - /** @var SenderInterface $emailSender */ - $emailSender = $container->get('sylius.email_sender'); - - $adminUser = new AdminUser(); - $adminUser->setEmail('sylius@example.com'); - $adminUser->setPasswordResetToken('my_reset_token'); - - $adminUserRepository = $this->createMock(UserRepositoryInterface::class); - $adminUserRepository - ->method('findOneByEmail') - ->with('sylius@example.com') - ->willReturn($adminUser) - ; - - $resetPasswordEmailHandler = new SendResetPasswordEmailHandler($adminUserRepository, $emailSender); - $resetPasswordEmailHandler(new SendResetPasswordEmail( - 'sylius@example.com', - 'en_US', - )); - - self::assertEmailCount(1); - $email = self::getMailerMessage(); - self::assertEmailAddressContains($email, 'To', 'sylius@example.com'); - self::assertEmailHtmlBodyContains( - $email, - $translator->trans('sylius.email.admin_password_reset.to_reset_your_password_token', [], null, 'en_US'), - ); - } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/Tests/Stub/CommandDataTransformerStub.php b/src/Sylius/Bundle/ApiBundle/Tests/Stub/CommandDataTransformerStub.php new file mode 100644 index 0000000000..2302d0a50d --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Tests/Stub/CommandDataTransformerStub.php @@ -0,0 +1,31 @@ + $orderRepository + */ + public function __construct(private OrderRepositoryInterface $orderRepository) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + Assert::isInstanceOf($value, ChangePaymentMethod::class); + + Assert::isInstanceOf($constraint, CanPaymentMethodBeChanged::class); + + /** @var OrderInterface|null $order */ + $order = $this->orderRepository->findOneByTokenValue($value->getOrderTokenValue()); + Assert::notNull($order); + + if ($order->getState() === OrderInterface::STATE_CANCELLED) { + $this->context->addViolation($constraint::CANNOT_CHANGE_PAYMENT_METHOD_FOR_CANCELLED_ORDER); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCart.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCart.php index 0e8e0abb79..4edb4bd3e2 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCart.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCart.php @@ -25,7 +25,7 @@ final class ChangedItemQuantityInCart extends Constraint public function validatedBy(): string { - return 'sylius_api_validator_changed_item_guantity_in_cart'; + return 'sylius_api_validator_changed_item_quantity_in_cart'; } public function getTargets(): string diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCartValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCartValidator.php index 7b9bf6fab2..911bf81ec7 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCartValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChangedItemQuantityInCartValidator.php @@ -27,6 +27,9 @@ use Webmozart\Assert\Assert; final class ChangedItemQuantityInCartValidator extends ConstraintValidator { + /** + * @param OrderItemRepositoryInterface $orderItemRepository + */ public function __construct( private OrderItemRepositoryInterface $orderItemRepository, private OrderRepositoryInterface $orderRepository, @@ -34,7 +37,7 @@ final class ChangedItemQuantityInCartValidator extends ConstraintValidator ) { } - public function validate($value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, ChangeItemQuantityInCart::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CheckoutCompletion.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CheckoutCompletion.php index 1aa3e574d4..3e72c921ee 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CheckoutCompletion.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CheckoutCompletion.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ class CheckoutCompletion extends Constraint { public string $message = 'sylius.order.invalid_state_transition'; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CheckoutCompletionValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CheckoutCompletionValidator.php index 8a533eab5f..962f87a08b 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CheckoutCompletionValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CheckoutCompletionValidator.php @@ -14,6 +14,9 @@ declare(strict_types=1); namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use SM\Factory\FactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\TransitionInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\OrderCheckoutTransitions; @@ -22,37 +25,64 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ class CheckoutCompletionValidator extends ConstraintValidator { + /** + * @param OrderRepositoryInterface $orderRepository + */ public function __construct( private OrderRepositoryInterface $orderRepository, - private FactoryInterface $stateMachineFactory, + private FactoryInterface|StateMachineInterface $stateMachineFactory, ) { + if ($this->stateMachineFactory instanceof FactoryInterface) { + trigger_deprecation( + 'sylius/api-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the second argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + FactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } - /** @param OrderTokenValueAwareInterface $value */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, OrderTokenValueAwareInterface::class); /** @var CheckoutCompletion $constraint */ Assert::isInstanceOf($constraint, CheckoutCompletion::class); + /** @var OrderInterface|null $order */ $order = $this->orderRepository->findOneBy(['tokenValue' => $value->getOrderTokenValue()]); - /** @var OrderInterface $order */ Assert::isInstanceOf($order, OrderInterface::class); - $stateMachine = $this->stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH); + $stateMachine = $this->getStateMachine(); - if ($stateMachine->can(OrderCheckoutTransitions::TRANSITION_COMPLETE)) { + if ($stateMachine->can($order, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_COMPLETE)) { return; } $this->context->addViolation($constraint->message, [ - '%currentState%' => $stateMachine->getState(), - '%possibleTransitions%' => implode(', ', $stateMachine->getPossibleTransitions()), + '%currentState%' => $order->getCheckoutState(), + '%possibleTransitions%' => implode( + ', ', + array_map( + fn (TransitionInterface $transition) => $transition->getName(), + $stateMachine->getEnabledTransitions($order, OrderCheckoutTransitions::GRAPH), + ), + ), ]); } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof FactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } } diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenPaymentMethodEligibilityValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenPaymentMethodEligibilityValidator.php index ad0607c711..f5824376cc 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenPaymentMethodEligibilityValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenPaymentMethodEligibilityValidator.php @@ -23,7 +23,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class ChosenPaymentMethodEligibilityValidator extends ConstraintValidator { public function __construct( @@ -33,7 +32,7 @@ final class ChosenPaymentMethodEligibilityValidator extends ConstraintValidator ) { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, ChoosePaymentMethod::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenShippingMethodEligibility.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenShippingMethodEligibility.php index fd6518c293..d72b9d214e 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenShippingMethodEligibility.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenShippingMethodEligibility.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class ChosenShippingMethodEligibility extends Constraint { public string $message = 'sylius.shipping_method.not_available'; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenShippingMethodEligibilityValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenShippingMethodEligibilityValidator.php index b2077660e3..e74b471032 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenShippingMethodEligibilityValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ChosenShippingMethodEligibilityValidator.php @@ -23,7 +23,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class ChosenShippingMethodEligibilityValidator extends ConstraintValidator { public function __construct( @@ -33,7 +32,7 @@ final class ChosenShippingMethodEligibilityValidator extends ConstraintValidator ) { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var ChooseShippingMethod $value */ Assert::isInstanceOf($value, ChooseShippingMethod::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/Code.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/Code.php new file mode 100644 index 0000000000..d34c151d19 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/Code.php @@ -0,0 +1,34 @@ + $options + */ + protected function getConstraints(array $options): array + { + return [ + new NotBlank(message: 'sylius.code.not_blank'), + new Type('string'), + new Regex(['pattern' => '/^[\w-]*$/'], message: 'sylius.code.invalid'), + ]; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ConfirmResetPassword.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ConfirmResetPassword.php index 926a1b2e34..a7bffc034d 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ConfirmResetPassword.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ConfirmResetPassword.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class ConfirmResetPassword extends Constraint { /** @var string */ diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ConfirmResetPasswordValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ConfirmResetPasswordValidator.php index edfb5de5c8..fac6b519f1 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ConfirmResetPasswordValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ConfirmResetPasswordValidator.php @@ -18,11 +18,9 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class ConfirmResetPasswordValidator extends ConstraintValidator { - /** @param ResetPassword|mixed $value */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, ResetPassword::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectChangeShopUserConfirmPassword.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectChangeShopUserConfirmPassword.php index c54a0d19eb..bb4aadcb3d 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectChangeShopUserConfirmPassword.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectChangeShopUserConfirmPassword.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class CorrectChangeShopUserConfirmPassword extends Constraint { /** @var string */ diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectChangeShopUserConfirmPasswordValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectChangeShopUserConfirmPasswordValidator.php index fab2ac75e5..ef0b6eed81 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectChangeShopUserConfirmPasswordValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectChangeShopUserConfirmPasswordValidator.php @@ -18,11 +18,9 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class CorrectChangeShopUserConfirmPasswordValidator extends ConstraintValidator { - /** @param ChangeShopUserPassword|mixed $value */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, ChangeShopUserPassword::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectOrderAddressValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectOrderAddressValidator.php index 5c11c78b7d..ba058a5781 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectOrderAddressValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/CorrectOrderAddressValidator.php @@ -27,7 +27,7 @@ final class CorrectOrderAddressValidator extends ConstraintValidator { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var UpdateCart $value */ Assert::isInstanceOf($value, UpdateCart::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderItemAvailability.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderItemAvailability.php index 1ef2351c32..c85c8b63f2 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderItemAvailability.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderItemAvailability.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class OrderItemAvailability extends Constraint { public string $message = 'sylius.product_variant.product_variant_with_name_not_sufficient'; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderItemAvailabilityValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderItemAvailabilityValidator.php index ac1d90a4dd..22aca0fd69 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderItemAvailabilityValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderItemAvailabilityValidator.php @@ -22,7 +22,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class OrderItemAvailabilityValidator extends ConstraintValidator { public function __construct( @@ -31,7 +30,7 @@ final class OrderItemAvailabilityValidator extends ConstraintValidator ) { } - public function validate($value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, OrderTokenValueAwareInterface::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderNotEmpty.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderNotEmpty.php index 31be632cdd..b6fc39d0ff 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderNotEmpty.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderNotEmpty.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class OrderNotEmpty extends Constraint { public string $message = 'sylius.order.not_empty'; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderNotEmptyValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderNotEmptyValidator.php index d519d60ba7..498d97116d 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderNotEmptyValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderNotEmptyValidator.php @@ -20,7 +20,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class OrderNotEmptyValidator extends ConstraintValidator { public function __construct(private OrderRepositoryInterface $orderRepository) @@ -30,7 +29,7 @@ final class OrderNotEmptyValidator extends ConstraintValidator /** * @throws \InvalidArgumentException */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, OrderTokenValueAwareInterface::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderPaymentMethodEligibility.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderPaymentMethodEligibility.php index 8024294338..f67f98bd63 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderPaymentMethodEligibility.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderPaymentMethodEligibility.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class OrderPaymentMethodEligibility extends Constraint { public string $message = 'sylius.order.payment_method_eligibility'; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderPaymentMethodEligibilityValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderPaymentMethodEligibilityValidator.php index 74c6247079..a5093eaebc 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderPaymentMethodEligibilityValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderPaymentMethodEligibilityValidator.php @@ -21,14 +21,13 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class OrderPaymentMethodEligibilityValidator extends ConstraintValidator { public function __construct(private OrderRepositoryInterface $orderRepository) { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, OrderTokenValueAwareInterface::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderProductEligibility.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderProductEligibility.php index a8d7ea3efe..4908236f8a 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderProductEligibility.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderProductEligibility.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class OrderProductEligibility extends Constraint { public string $message = 'sylius.order.product_eligibility'; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderProductEligibilityValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderProductEligibilityValidator.php index 630863b620..41ae7423f3 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderProductEligibilityValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderProductEligibilityValidator.php @@ -21,7 +21,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class OrderProductEligibilityValidator extends ConstraintValidator { public function __construct(private OrderRepositoryInterface $orderRepository) @@ -31,7 +30,7 @@ final class OrderProductEligibilityValidator extends ConstraintValidator /** * @throws \InvalidArgumentException */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, OrderTokenValueAwareInterface::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderShippingMethodEligibility.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderShippingMethodEligibility.php index 4a492cbf86..66d3feabeb 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderShippingMethodEligibility.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderShippingMethodEligibility.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class OrderShippingMethodEligibility extends Constraint { /** diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderShippingMethodEligibilityValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderShippingMethodEligibilityValidator.php index 053ae41fbc..f4591c3216 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderShippingMethodEligibilityValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/OrderShippingMethodEligibilityValidator.php @@ -23,7 +23,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class OrderShippingMethodEligibilityValidator extends ConstraintValidator { public function __construct( @@ -32,7 +31,7 @@ final class OrderShippingMethodEligibilityValidator extends ConstraintValidator ) { } - public function validate(mixed $value, Constraint $constraint) + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, OrderTokenValueAwareInterface::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/PromotionCouponEligibility.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/PromotionCouponEligibility.php index b9cc3bef42..0c5aeb45a0 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/PromotionCouponEligibility.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/PromotionCouponEligibility.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class PromotionCouponEligibility extends Constraint { /** @var string */ diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/PromotionCouponEligibilityValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/PromotionCouponEligibilityValidator.php index ed1874915e..f4e33b797c 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/PromotionCouponEligibilityValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/PromotionCouponEligibilityValidator.php @@ -23,7 +23,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class PromotionCouponEligibilityValidator extends ConstraintValidator { public function __construct( @@ -33,7 +32,7 @@ final class PromotionCouponEligibilityValidator extends ConstraintValidator ) { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, UpdateCart::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShipmentAlreadyShippedValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShipmentAlreadyShippedValidator.php index 31cdc997c9..688c68f9ab 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShipmentAlreadyShippedValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShipmentAlreadyShippedValidator.php @@ -27,7 +27,7 @@ final class ShipmentAlreadyShippedValidator extends ConstraintValidator { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($constraint, ShipmentAlreadyShipped::class); Assert::isInstanceOf($value, ShipShipment::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserNotVerified.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserNotVerified.php index e39f9f28cb..2a776d3d38 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserNotVerified.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserNotVerified.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class ShopUserNotVerified extends Constraint { public string $message = 'sylius.account.is_verified'; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserNotVerifiedValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserNotVerifiedValidator.php index 859b78d118..a0ff4f9035 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserNotVerifiedValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserNotVerifiedValidator.php @@ -20,14 +20,13 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class ShopUserNotVerifiedValidator extends ConstraintValidator { public function __construct(private UserRepositoryInterface $shopUserRepository) { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, ShopUserIdAwareInterface::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserResetPasswordTokenExists.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserResetPasswordTokenExists.php new file mode 100644 index 0000000000..b2a17e94a3 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserResetPasswordTokenExists.php @@ -0,0 +1,26 @@ + $shopUserRepository + */ + public function __construct(private UserRepositoryInterface $shopUserRepository) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + Assert::string($value); + + /** @var ShopUserResetPasswordTokenExists $constraint */ + Assert::isInstanceOf($constraint, ShopUserResetPasswordTokenExists::class); + + /** @var UserInterface|null $user */ + $user = $this->shopUserRepository->findOneBy(['passwordResetToken' => $value]); + + if (null !== $user) { + return; + } + + $this->context->addViolation($constraint->message, ['%token%' => $value]); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserResetPasswordTokenNotExpired.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserResetPasswordTokenNotExpired.php new file mode 100644 index 0000000000..ed7a3885ab --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/ShopUserResetPasswordTokenNotExpired.php @@ -0,0 +1,26 @@ + $userRepository + */ + public function __construct( + private UserRepositoryInterface $userRepository, + private string $passwordResetTokenTtl, + ) { + } + + public function validate(mixed $value, Constraint $constraint): void + { + Assert::string($value); + + /** @var ShopUserResetPasswordTokenNotExpired $constraint */ + Assert::isInstanceOf($constraint, ShopUserResetPasswordTokenNotExpired::class); + + /** @var UserInterface|null $user */ + $user = $this->userRepository->findOneBy(['passwordResetToken' => $value]); + + if (null === $user) { + return; + } + + $lifetime = new \DateInterval($this->passwordResetTokenTtl); + + if ($user->isPasswordRequestNonExpired($lifetime)) { + return; + } + + $this->context->addViolation($constraint->message); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/SingleValueForProductVariantOption.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/SingleValueForProductVariantOption.php new file mode 100644 index 0000000000..d979a06885 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/SingleValueForProductVariantOption.php @@ -0,0 +1,31 @@ + $productOptionValue->getOptionCode(), $value->getOptionValues()->toArray()); + /** @var array $flippedMap */ + $flippedMap = array_flip($map); + if (count($map) !== count($flippedMap)) { + $this->context->addViolation($constraint->message); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueReviewerEmail.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueReviewerEmail.php index 0dc107ad2e..dd51b35616 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueReviewerEmail.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueReviewerEmail.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class UniqueReviewerEmail extends Constraint { public string $message = 'sylius.review.author.already_exists'; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueReviewerEmailValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueReviewerEmailValidator.php index 15167aa1a7..23d7c0f6f9 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueReviewerEmailValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueReviewerEmailValidator.php @@ -21,7 +21,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class UniqueReviewerEmailValidator extends ConstraintValidator { public function __construct( @@ -30,7 +29,7 @@ final class UniqueReviewerEmailValidator extends ConstraintValidator ) { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if ($value === null) { return; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueShopUserEmail.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueShopUserEmail.php index 1f4fcadfa3..35b23e5812 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueShopUserEmail.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueShopUserEmail.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class UniqueShopUserEmail extends Constraint { public string $message = 'sylius.user.email.unique'; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueShopUserEmailValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueShopUserEmailValidator.php index f4c849836b..a3bc53f3b3 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueShopUserEmailValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UniqueShopUserEmailValidator.php @@ -19,7 +19,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class UniqueShopUserEmailValidator extends ConstraintValidator { public function __construct( @@ -28,7 +27,7 @@ final class UniqueShopUserEmailValidator extends ConstraintValidator ) { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if ($value === null) { return; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UpdateCartEmailNotAllowed.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UpdateCartEmailNotAllowed.php index f8e162f040..00281a947d 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UpdateCartEmailNotAllowed.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UpdateCartEmailNotAllowed.php @@ -15,7 +15,6 @@ namespace Sylius\Bundle\ApiBundle\Validator\Constraints; use Symfony\Component\Validator\Constraint; -/** @experimental */ final class UpdateCartEmailNotAllowed extends Constraint { public string $message = 'sylius.checkout.email.not_changeable'; diff --git a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UpdateCartEmailNotAllowedValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UpdateCartEmailNotAllowedValidator.php index 2aa9f5798e..0efeb2af85 100644 --- a/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UpdateCartEmailNotAllowedValidator.php +++ b/src/Sylius/Bundle/ApiBundle/Validator/Constraints/UpdateCartEmailNotAllowedValidator.php @@ -22,7 +22,6 @@ use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; -/** @experimental */ final class UpdateCartEmailNotAllowedValidator extends ConstraintValidator { public function __construct( @@ -31,7 +30,7 @@ final class UpdateCartEmailNotAllowedValidator extends ConstraintValidator ) { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, OrderTokenValueAwareInterface::class); Assert::isInstanceOf($value, CustomerEmailAwareInterface::class); diff --git a/src/Sylius/Bundle/ApiBundle/Validator/ResourceApiInputDataPropertiesValidator.php b/src/Sylius/Bundle/ApiBundle/Validator/ResourceApiInputDataPropertiesValidator.php new file mode 100644 index 0000000000..2d50ce8ada --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Validator/ResourceApiInputDataPropertiesValidator.php @@ -0,0 +1,46 @@ +validator->startContext()->getViolations(); + foreach ($inputData as $key => $value) { + $propertyViolations = $this->validator->validatePropertyValue( + $resource, + $key, + $value, + $validationGroups, + ); + + if ($propertyViolations->count() > 0) { + $violations->addAll($propertyViolations); + } + } + + if ($violations->count() > 0) { + throw new ValidationException($violations); + } + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Validator/ResourceInputDataPropertiesValidatorInterface.php b/src/Sylius/Bundle/ApiBundle/Validator/ResourceInputDataPropertiesValidatorInterface.php new file mode 100644 index 0000000000..60107186d0 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/Validator/ResourceInputDataPropertiesValidatorInterface.php @@ -0,0 +1,21 @@ +shouldImplement(MetadataMergerInterface::class); + } + + function it_does_nothing_when_both_new_and_old_metadata_are_epmty(): void + { + $this->merge([], [])->shouldReturn([]); + } + + function it_returns_old_metadata_as_is_when_new_metadata_is_empty(): void + { + $this->merge(['foo' => 'bar'], [])->shouldReturn(['foo' => 'bar']); + } + + function it_returns_new_metadata_as_is_when_old_metadata_is_empty(): void + { + $this->merge([], ['foo' => 'bar'])->shouldReturn(['foo' => 'bar']); + } + + function it_ignores_properties_when_merging(): void + { + $this->merge( + ['properties' => ['foo' => 'bar']], + ['properties' => ['foo' => 'baz']], + )->shouldReturn(['properties' => ['foo' => 'bar']]); + } + + function it_adds_metadata_missing_from_old_metadata_as_they_are(): void + { + $this->merge( + ['foo' => 'bar'], + ['baz' => 'qux'], + )->shouldReturn(['foo' => 'bar', 'baz' => 'qux']); + } + + function it_overrides_metadata_present_in_both_old_and_new_metadata(): void + { + $this->merge( + ['foo' => 'bar'], + ['foo' => 'baz'], + )->shouldReturn(['foo' => 'baz']); + } + + function it_adds_new_collection_operations_when_old_metadata_has_none(): void + { + $this->merge( + [], + ['collectionOperations' => ['get' => ['baz' => 'qux']]], + )->shouldReturn(['collectionOperations' => ['get' => ['baz' => 'qux']]]); + } + + function it_adds_new_collection_operations_when_old_metadata_did_not_have_them(): void + { + $this->merge( + ['collectionOperations' => ['post' => ['foo' => 'bar']]], + ['collectionOperations' => ['get' => ['baz' => 'qux']]], + )->shouldReturn(['collectionOperations' => [ + 'post' => ['foo' => 'bar'], + 'get' => ['baz' => 'qux'], + ]]); + } + + function it_merges_collection_operations(): void + { + $this->merge([ + 'collectionOperations' => [ + 'get' => ['foo' => 'bar'], + ], + ], [ + 'collectionOperations' => [ + 'get' => ['baz' => 'qux'], + ], + ])->shouldReturn([ + 'collectionOperations' => [ + 'get' => ['foo' => 'bar', 'baz' => 'qux'], + ], + ]); + } + + function it_adds_new_item_operations_when_old_metadata_has_none(): void + { + $this->merge( + [], + ['itemOperations' => ['get' => ['baz' => 'qux']]], + )->shouldReturn(['itemOperations' => ['get' => ['baz' => 'qux']]]); + } + + function it_adds_new_item_operations_when_old_metadata_did_not_have_them(): void + { + $this->merge( + ['itemOperations' => ['post' => ['foo' => 'bar']]], + ['itemOperations' => ['get' => ['baz' => 'qux']]], + )->shouldReturn(['itemOperations' => [ + 'post' => ['foo' => 'bar'], + 'get' => ['baz' => 'qux'], + ]]); + } + + function it_merges_item_operations(): void + { + $this->merge([ + 'itemOperations' => [ + 'get' => ['foo' => 'bar'], + ], + ], [ + 'itemOperations' => [ + 'get' => ['baz' => 'qux'], + ], + ])->shouldReturn([ + 'itemOperations' => [ + 'get' => ['foo' => 'bar', 'baz' => 'qux'], + ], + ]); + } + + function it_adds_new_subresource_operations_when_old_metadata_has_none(): void + { + $this->merge( + [], + ['subresourceOperations' => ['get' => ['baz' => 'qux']]], + )->shouldReturn(['subresourceOperations' => ['get' => ['baz' => 'qux']]]); + } + + function it_adds_new_subresource_operations_when_old_metadata_did_not_have_them(): void + { + $this->merge( + ['subresourceOperations' => ['post' => ['foo' => 'bar']]], + ['subresourceOperations' => ['get' => ['baz' => 'qux']]], + )->shouldReturn(['subresourceOperations' => [ + 'post' => ['foo' => 'bar'], + 'get' => ['baz' => 'qux'], + ]]); + } + + function it_merges_subresource_operations(): void + { + $this->merge([ + 'subresourceOperations' => [ + 'get' => ['foo' => 'bar'], + ], + ], [ + 'subresourceOperations' => [ + 'get' => ['baz' => 'qux'], + ], + ])->shouldReturn([ + 'subresourceOperations' => [ + 'get' => ['foo' => 'bar', 'baz' => 'qux'], + ], + ]); + } + + function it_merges_complex_metadata(): void + { + $this->merge([ + 'validation_groups' => ['Default', 'sylius'], + 'route_prefix' => 'old', + 'collectionOperations' => [ + 'get' => [ + 'method' => 'GET', + 'path' => '/old', + 'controller' => 'old', + 'defaults' => ['_controller' => 'old'], + 'requirements' => ['_format' => 'old'], + 'options' => ['foo' => 'bar'], + 'openapi_context' => ['foo' => 'bar'], + ], + ], + 'itemOperations' => [ + 'get' => [ + 'method' => 'GET', + 'path' => '/old/{id}', + 'controller' => 'old', + 'defaults' => ['_controller' => 'old'], + 'requirements' => ['_format' => 'old'], + 'options' => ['foo' => 'bar'], + 'openapi_context' => ['foo' => 'bar'], + ], + ], + 'subresourceOperations' => [ + 'get' => [ + 'method' => 'GET', + 'path' => '/old/{id}/old', + 'controller' => 'old', + 'defaults' => ['_controller' => 'old'], + 'requirements' => ['_format' => 'old'], + 'options' => ['foo' => 'bar'], + 'openapi_context' => ['foo' => 'bar'], + ], + ], + 'properties' => [ + 'foo' => [ + 'description' => 'old', + 'readable' => true, + 'writable' => true, + 'required' => true, + 'identifier' => true, + ], + ], + ], [ + 'validation_groups' => 'sylius', + 'collectionOperations' => [ + 'get' => [ + 'method' => 'GET', + 'path' => '/new', + 'controller' => 'new', + ], + ], + 'itemOperations' => [ + 'post' => [ + 'method' => 'POST', + 'path' => '/new/{id}', + 'controller' => 'new', + ], + ], + 'properties' => [ + 'bar' => [ + 'description' => 'new', + 'readable' => true, + 'writable' => true, + 'required' => false, + 'readableLink' => true, + 'identifier' => false, + ], + ], + ])->shouldIterateLike([ + 'validation_groups' => 'sylius', + 'route_prefix' => 'old', + 'collectionOperations' => [ + 'get' => [ + 'method' => 'GET', + 'path' => '/new', + 'controller' => 'new', + 'defaults' => ['_controller' => 'old'], + 'requirements' => ['_format' => 'old'], + 'options' => ['foo' => 'bar'], + 'openapi_context' => ['foo' => 'bar'], + ], + ], + 'itemOperations' => [ + 'get' => [ + 'method' => 'GET', + 'path' => '/old/{id}', + 'controller' => 'old', + 'defaults' => ['_controller' => 'old'], + 'requirements' => ['_format' => 'old'], + 'options' => ['foo' => 'bar'], + 'openapi_context' => ['foo' => 'bar'], + ], + 'post' => [ + 'method' => 'POST', + 'path' => '/new/{id}', + 'controller' => 'new', + ], + ], + 'subresourceOperations' => [ + 'get' => [ + 'method' => 'GET', + 'path' => '/old/{id}/old', + 'controller' => 'old', + 'defaults' => ['_controller' => 'old'], + 'requirements' => ['_format' => 'old'], + 'options' => ['foo' => 'bar'], + 'openapi_context' => ['foo' => 'bar'], + ], + ], + 'properties' => [ + 'foo' => [ + 'description' => 'old', + 'readable' => true, + 'writable' => true, + 'required' => true, + 'identifier' => true, + ], + ], + ]); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Applicator/ArchivingPromotionApplicatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Applicator/ArchivingPromotionApplicatorSpec.php new file mode 100644 index 0000000000..58bb26a1f4 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Applicator/ArchivingPromotionApplicatorSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($calendar); + } + + function it_archives_promotion( + DateTimeProviderInterface $calendar, + PromotionInterface $promotion, + ): void { + $now = new \DateTime(); + $calendar->now()->willReturn($now); + + $promotion->setArchivedAt($now)->shouldBeCalledOnce(); + + $this->archive($promotion)->shouldReturn($promotion); + } + + function it_restores_promotion( + PromotionInterface $promotion, + ): void { + $promotion->setArchivedAt(null)->shouldBeCalledOnce(); + + $this->restore($promotion)->shouldReturn($promotion); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Applicator/OrderStateMachineTransitionApplicatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Applicator/OrderStateMachineTransitionApplicatorSpec.php index 9a25b5a4e2..2079b62473 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Applicator/OrderStateMachineTransitionApplicatorSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Applicator/OrderStateMachineTransitionApplicatorSpec.php @@ -15,7 +15,9 @@ namespace spec\Sylius\Bundle\ApiBundle\Applicator; use PhpSpec\ObjectBehavior; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; -use SM\StateMachine\StateMachine; +use SM\StateMachine\StateMachine as WinzouStateMachine; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Bundle\ApiBundle\Exception\StateMachineTransitionFailedException; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Order\OrderTransitions; @@ -29,11 +31,52 @@ final class OrderStateMachineTransitionApplicatorSpec extends ObjectBehavior function it_cancels_order( StateMachineFactoryInterface $stateMachineFactory, OrderInterface $order, - StateMachine $stateMachine, + WinzouStateMachine $stateMachine, ): void { $stateMachineFactory->get($order, OrderTransitions::GRAPH)->willReturn($stateMachine); + $stateMachine->can(OrderTransitions::TRANSITION_CANCEL)->willReturn(true); $stateMachine->apply(OrderTransitions::TRANSITION_CANCEL)->shouldBeCalled(); $this->cancel($order); } + + function it_throw_exception_if_cannot_cancel_order( + StateMachineFactoryInterface $stateMachineFactory, + OrderInterface $order, + WinzouStateMachine $stateMachine, + ): void { + $stateMachineFactory->get($order, OrderTransitions::GRAPH)->willReturn($stateMachine); + $stateMachine->can(OrderTransitions::TRANSITION_CANCEL)->willReturn(false); + $stateMachine->apply(OrderTransitions::TRANSITION_CANCEL)->shouldNotBeCalled(); + + $this + ->shouldThrow(StateMachineTransitionFailedException::class) + ->during('cancel', [$order]) + ; + } + + function it_uses_the_new_state_machine_abstraction_if_passed( + StateMachineInterface $stateMachine, + OrderInterface $order, + ): void { + $this->beConstructedWith($stateMachine); + $stateMachine->can($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CANCEL)->willReturn(true); + $stateMachine->apply($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CANCEL)->shouldBeCalled(); + + $this->cancel($order); + } + + function it_throw_exception_if_cannot_cancel_order_with_new_state_machine_abstraction( + StateMachineInterface $stateMachine, + OrderInterface $order, + ): void { + $this->beConstructedWith($stateMachine); + $stateMachine->can($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CANCEL)->willReturn(false); + $stateMachine->apply($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CANCEL)->shouldNotBeCalled(); + + $this + ->shouldThrow(StateMachineTransitionFailedException::class) + ->during('cancel', [$order]) + ; + } } diff --git a/src/Sylius/Bundle/ApiBundle/spec/Applicator/PaymentStateMachineTransitionApplicatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Applicator/PaymentStateMachineTransitionApplicatorSpec.php index 6d2514ae6f..3b568d505b 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Applicator/PaymentStateMachineTransitionApplicatorSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Applicator/PaymentStateMachineTransitionApplicatorSpec.php @@ -15,7 +15,9 @@ namespace spec\Sylius\Bundle\ApiBundle\Applicator; use PhpSpec\ObjectBehavior; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; -use SM\StateMachine\StateMachine; +use SM\StateMachine\StateMachine as WinzouStateMachine; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Bundle\ApiBundle\Exception\StateMachineTransitionFailedException; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Payment\PaymentTransitions; @@ -29,11 +31,54 @@ final class PaymentStateMachineTransitionApplicatorSpec extends ObjectBehavior function it_completes_payment( StateMachineFactoryInterface $stateMachineFactory, PaymentInterface $payment, - StateMachine $stateMachine, + WinzouStateMachine $stateMachine, ): void { $stateMachineFactory->get($payment, PaymentTransitions::GRAPH)->willReturn($stateMachine); + $stateMachine->can(PaymentTransitions::TRANSITION_COMPLETE)->willReturn(true); $stateMachine->apply(PaymentTransitions::TRANSITION_COMPLETE)->shouldBeCalled(); $this->complete($payment); } + + function it_throws_exception_if_cannot_complete_payment( + StateMachineFactoryInterface $stateMachineFactory, + PaymentInterface $payment, + WinzouStateMachine $stateMachine, + ): void { + $stateMachineFactory->get($payment, PaymentTransitions::GRAPH)->willReturn($stateMachine); + $stateMachine->can(PaymentTransitions::TRANSITION_COMPLETE)->willReturn(false); + $stateMachine->apply(PaymentTransitions::TRANSITION_COMPLETE)->shouldNotBeCalled(); + + $this + ->shouldThrow(StateMachineTransitionFailedException::class) + ->during('complete', [$payment]) + ; + } + + function it_uses_the_new_state_machine_abstraction_if_passed( + StateMachineInterface $stateMachine, + PaymentInterface $payment, + ): void { + $this->beConstructedWith($stateMachine); + + $stateMachine->can($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_COMPLETE)->willReturn(true); + $stateMachine->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_COMPLETE)->shouldBeCalled(); + + $this->complete($payment); + } + + function it_throws_exception_if_cannot_complete_payment_with_new_state_machine_abstraction( + StateMachineInterface $stateMachine, + PaymentInterface $payment, + ): void { + $this->beConstructedWith($stateMachine); + + $stateMachine->can($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_COMPLETE)->willReturn(false); + $stateMachine->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_COMPLETE)->shouldNotBeCalled(); + + $this + ->shouldThrow(StateMachineTransitionFailedException::class) + ->during('complete', [$payment]) + ; + } } diff --git a/src/Sylius/Bundle/ApiBundle/spec/Applicator/ProductReviewStateMachineTransitionApplicatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Applicator/ProductReviewStateMachineTransitionApplicatorSpec.php index 134f7fd531..217613b5f3 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Applicator/ProductReviewStateMachineTransitionApplicatorSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Applicator/ProductReviewStateMachineTransitionApplicatorSpec.php @@ -15,7 +15,9 @@ namespace spec\Sylius\Bundle\ApiBundle\Applicator; use PhpSpec\ObjectBehavior; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; -use SM\StateMachine\StateMachine; +use SM\StateMachine\StateMachine as WinzouStateMachine; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Bundle\ApiBundle\Exception\StateMachineTransitionFailedException; use Sylius\Component\Core\ProductReviewTransitions; use Sylius\Component\Review\Model\ReviewInterface; @@ -26,25 +28,110 @@ final class ProductReviewStateMachineTransitionApplicatorSpec extends ObjectBeha $this->beConstructedWith($stateMachineFactory); } - public function it_accepts_product_review( + function it_accepts_product_review( StateMachineFactoryInterface $stateMachineFactory, ReviewInterface $review, - StateMachine $stateMachine, + WinzouStateMachine $stateMachine, ): void { $stateMachineFactory->get($review, ProductReviewTransitions::GRAPH)->willReturn($stateMachine); + $stateMachine->can(ProductReviewTransitions::TRANSITION_ACCEPT)->willReturn(true); $stateMachine->apply(ProductReviewTransitions::TRANSITION_ACCEPT)->shouldBeCalled(); $this->accept($review); } - public function it_rejects_product_review( + function it_throws_exception_if_cannot_accept_product_review( StateMachineFactoryInterface $stateMachineFactory, ReviewInterface $review, - StateMachine $stateMachine, + WinzouStateMachine $stateMachine, ): void { $stateMachineFactory->get($review, ProductReviewTransitions::GRAPH)->willReturn($stateMachine); + $stateMachine->can(ProductReviewTransitions::TRANSITION_ACCEPT)->willReturn(false); + $stateMachine->apply(ProductReviewTransitions::TRANSITION_ACCEPT)->shouldNotBeCalled(); + + $this + ->shouldThrow(StateMachineTransitionFailedException::class) + ->during('accept', [$review]) + ; + } + + function it_uses_the_new_state_machine_abstraction_if_passed_while_accepting_a_product_review( + StateMachineInterface $stateMachine, + ReviewInterface $review, + ): void { + $this->beConstructedWith($stateMachine); + + $stateMachine->can($review, ProductReviewTransitions::GRAPH, ProductReviewTransitions::TRANSITION_ACCEPT)->willReturn(true); + $stateMachine->apply($review, ProductReviewTransitions::GRAPH, ProductReviewTransitions::TRANSITION_ACCEPT)->shouldBeCalled(); + + $this->accept($review); + } + + function it_throws_exception_if_cannot_accept_product_review_with_new_state_machine_abstraction( + StateMachineInterface $stateMachine, + ReviewInterface $review, + ): void { + $this->beConstructedWith($stateMachine); + + $stateMachine->can($review, ProductReviewTransitions::GRAPH, ProductReviewTransitions::TRANSITION_ACCEPT)->willReturn(false); + $stateMachine->apply($review, ProductReviewTransitions::GRAPH, ProductReviewTransitions::TRANSITION_ACCEPT)->shouldNotBeCalled(); + + $this + ->shouldThrow(StateMachineTransitionFailedException::class) + ->during('accept', [$review]) + ; + } + + function it_rejects_product_review( + StateMachineFactoryInterface $stateMachineFactory, + ReviewInterface $review, + WinzouStateMachine $stateMachine, + ): void { + $stateMachineFactory->get($review, ProductReviewTransitions::GRAPH)->willReturn($stateMachine); + $stateMachine->can(ProductReviewTransitions::TRANSITION_REJECT)->willReturn(true); $stateMachine->apply(ProductReviewTransitions::TRANSITION_REJECT)->shouldBeCalled(); $this->reject($review); } + + function it_throws_exception_if_cannot_reject_product_review( + StateMachineFactoryInterface $stateMachineFactory, + ReviewInterface $review, + WinzouStateMachine $stateMachine, + ): void { + $stateMachineFactory->get($review, ProductReviewTransitions::GRAPH)->willReturn($stateMachine); + $stateMachine->can(ProductReviewTransitions::TRANSITION_REJECT)->willReturn(false); + $stateMachine->apply(ProductReviewTransitions::TRANSITION_REJECT)->shouldNotBeCalled(); + + $this + ->shouldThrow(StateMachineTransitionFailedException::class) + ->during('reject', [$review]) + ; + } + + function it_uses_the_new_state_machine_abstraction_if_passed_while_rejecting_a_product_review( + StateMachineInterface $stateMachine, + ReviewInterface $review, + ): void { + $this->beConstructedWith($stateMachine); + + $stateMachine->can($review, ProductReviewTransitions::GRAPH, ProductReviewTransitions::TRANSITION_REJECT)->willReturn(true); + $stateMachine->apply($review, ProductReviewTransitions::GRAPH, ProductReviewTransitions::TRANSITION_REJECT)->shouldBeCalled(); + + $this->reject($review); + } + + function it_throws_exception_if_cannot_reject_product_review_with_new_state_machine_abstraction( + StateMachineInterface $stateMachine, + ReviewInterface $review, + ): void { + $this->beConstructedWith($stateMachine); + + $stateMachine->can($review, ProductReviewTransitions::GRAPH, ProductReviewTransitions::TRANSITION_REJECT)->willReturn(false); + $stateMachine->apply($review, ProductReviewTransitions::GRAPH, ProductReviewTransitions::TRANSITION_REJECT)->shouldNotBeCalled(); + + $this + ->shouldThrow(StateMachineTransitionFailedException::class) + ->during('reject', [$review]); + } } diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/SendAccountRegistrationEmailHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/SendAccountRegistrationEmailHandlerSpec.php new file mode 100644 index 0000000000..e685f75f99 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/SendAccountRegistrationEmailHandlerSpec.php @@ -0,0 +1,93 @@ +beConstructedWith($shopUserRepository, $channelRepository, $accountRegistrationEmailManager); + } + + function it_sends_user_account_registration_email_when_account_verification_is_not_required( + UserRepositoryInterface $shopUserRepository, + ChannelRepositoryInterface $channelRepository, + AccountRegistrationEmailManagerInterface $accountRegistrationEmailManager, + ShopUserInterface $shopUser, + ChannelInterface $channel, + ): void { + $shopUserRepository->findOneByEmail('shop@example.com')->willReturn($shopUser); + $channelRepository->findOneByCode('WEB')->willReturn($channel); + + $channel->isAccountVerificationRequired()->willReturn(false); + + $accountRegistrationEmailManager + ->sendAccountRegistrationEmail($shopUser, $channel, 'en_US') + ->shouldBeCalled() + ; + + $this(new SendAccountRegistrationEmail('shop@example.com', 'en_US', 'WEB')); + } + + function it_sends_user_registration_email_when_account_verification_required_and_user_is_enabled( + UserRepositoryInterface $shopUserRepository, + ChannelRepositoryInterface $channelRepository, + AccountRegistrationEmailManagerInterface $accountRegistrationEmailManager, + ShopUserInterface $shopUser, + ChannelInterface $channel, + ): void { + $shopUserRepository->findOneByEmail('shop@example.com')->willReturn($shopUser); + $channelRepository->findOneByCode('WEB')->willReturn($channel); + + $channel->isAccountVerificationRequired()->willReturn(true); + $shopUser->isEnabled()->willReturn(true); + + $accountRegistrationEmailManager + ->sendAccountRegistrationEmail($shopUser, $channel, 'en_US') + ->shouldBeCalled() + ; + + $this(new SendAccountRegistrationEmail('shop@example.com', 'en_US', 'WEB')); + } + + function it_does_nothing_when_account_verification_is_required_and_user_is_disabled( + UserRepositoryInterface $shopUserRepository, + ChannelRepositoryInterface $channelRepository, + AccountRegistrationEmailManagerInterface $accountRegistrationEmailManager, + ShopUserInterface $shopUser, + ChannelInterface $channel, + ): void { + $shopUserRepository->findOneByEmail('shop@example.com')->willReturn($shopUser); + $channelRepository->findOneByCode('WEB')->willReturn($channel); + + $channel->isAccountVerificationRequired()->willReturn(true); + $shopUser->isEnabled()->willReturn(false); + + $accountRegistrationEmailManager->sendAccountRegistrationEmail(Argument::cetera())->shouldNotBeCalled(); + + $this(new SendAccountRegistrationEmail('shop@example.com', 'en_US', 'WEB')); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/SendAccountVerificationEmailHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/SendAccountVerificationEmailHandlerSpec.php new file mode 100644 index 0000000000..a91cc0892b --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/SendAccountVerificationEmailHandlerSpec.php @@ -0,0 +1,91 @@ +beConstructedWith($shopUserRepository, $channelRepository, $accountVerificationEmailManager); + } + + function it_sends_user_account_verification_email( + UserRepositoryInterface $shopUserRepository, + ChannelRepositoryInterface $channelRepository, + AccountVerificationEmailManagerInterface $accountVerificationEmailManager, + ShopUserInterface $shopUser, + ChannelInterface $channel, + ): void { + $shopUserRepository->findOneByEmail('shop@example.com')->willReturn($shopUser); + $channelRepository->findOneByCode('WEB')->willReturn($channel); + + $channel->isAccountVerificationRequired()->willReturn(false); + + $accountVerificationEmailManager + ->sendAccountVerificationEmail($shopUser, $channel, 'en_US') + ->shouldBeCalled() + ; + + $this(new SendAccountVerificationEmail('shop@example.com', 'en_US', 'WEB')); + } + + function it_throws_an_exception_if_user_has_not_been_found( + UserRepositoryInterface $shopUserRepository, + ChannelRepositoryInterface $channelRepository, + AccountVerificationEmailManagerInterface $accountVerificationEmailManager, + ): void { + $shopUserRepository->findOneByEmail('shop@example.com')->willReturn(null); + $channelRepository->findOneByCode('WEB')->shouldNotBeCalled(); + $accountVerificationEmailManager->sendAccountVerificationEmail(Argument::cetera())->shouldNotBeCalled(); + + $this + ->shouldThrow(UserNotFoundException::class) + ->during( + '__invoke', + [new SendAccountVerificationEmail('shop@example.com', 'en_US', 'WEB')], + ); + } + + function it_throws_an_exception_if_channel_has_not_been_found( + UserRepositoryInterface $shopUserRepository, + ChannelRepositoryInterface $channelRepository, + AccountVerificationEmailManagerInterface $accountVerificationEmailManager, + ShopUserInterface $shopUser, + ): void { + $shopUserRepository->findOneByEmail('shop@example.com')->willReturn($shopUser); + $channelRepository->findOneByCode('WEB')->willReturn(null); + $accountVerificationEmailManager->sendAccountVerificationEmail(Argument::cetera())->shouldNotBeCalled(); + + $this + ->shouldThrow(ChannelNotFoundException::class) + ->during( + '__invoke', + [new SendAccountVerificationEmail('shop@example.com', 'en_US', 'WEB')], + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/SendResetPasswordEmailHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/SendResetPasswordEmailHandlerSpec.php index 785828426e..905271b60a 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/SendResetPasswordEmailHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/SendResetPasswordEmailHandlerSpec.php @@ -15,10 +15,9 @@ namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Account; use PhpSpec\ObjectBehavior; use Sylius\Bundle\ApiBundle\Command\Account\SendResetPasswordEmail; -use Sylius\Bundle\CoreBundle\Mailer\Emails; +use Sylius\Bundle\CoreBundle\Mailer\ResetPasswordEmailManagerInterface; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; use Sylius\Component\Core\Model\ChannelInterface; -use Sylius\Component\Mailer\Sender\SenderInterface; use Sylius\Component\User\Model\UserInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; @@ -26,11 +25,11 @@ use Symfony\Component\Messenger\Handler\MessageHandlerInterface; final class SendResetPasswordEmailHandlerSpec extends ObjectBehavior { function let( - SenderInterface $emailSender, ChannelRepositoryInterface $channelRepository, UserRepositoryInterface $userRepository, + ResetPasswordEmailManagerInterface $resetPasswordEmailManager, ): void { - $this->beConstructedWith($emailSender, $channelRepository, $userRepository); + $this->beConstructedWith($channelRepository, $userRepository, $resetPasswordEmailManager); } function it_is_a_message_handler(): void @@ -39,32 +38,17 @@ final class SendResetPasswordEmailHandlerSpec extends ObjectBehavior } function it_sends_message_with_reset_password_token( - SenderInterface $sender, - UserRepositoryInterface $userRepository, - SendResetPasswordEmail $sendResetPasswordEmail, - UserInterface $user, ChannelRepositoryInterface $channelRepository, + UserRepositoryInterface $userRepository, + ResetPasswordEmailManagerInterface $resetPasswordEmailManager, + UserInterface $user, ChannelInterface $channel, ): void { - $sendResetPasswordEmail->email()->willReturn('iAmAnEmail@spaghettiCode.php'); - $userRepository->findOneByEmail('iAmAnEmail@spaghettiCode.php')->willReturn($user); - $sendResetPasswordEmail->channelCode()->willReturn('WEB'); - $channelRepository->findOneByCode('WEB')->willReturn($channel); - $sendResetPasswordEmail->localeCode()->willReturn('en_US'); - - $sender->send( - Emails::PASSWORD_RESET, - ['iAmAnEmail@spaghettiCode.php'], - [ - 'user' => $user->getWrappedObject(), - 'localeCode' => 'en_US', - 'channel' => $channel->getWrappedObject(), - ], - ); + $resetPasswordEmailManager->sendResetPasswordEmail($user, $channel, 'en_US')->shouldBeCalled(); $this(new SendResetPasswordEmail('iAmAnEmail@spaghettiCode.php', 'WEB', 'en_US')); } diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/VerifyCustomerAccountHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/VerifyCustomerAccountHandlerSpec.php index 57589bf4f6..f4f868d433 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/VerifyCustomerAccountHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Account/VerifyCustomerAccountHandlerSpec.php @@ -15,19 +15,24 @@ namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Account; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +use Sylius\Bundle\ApiBundle\Command\Account\SendAccountRegistrationEmail; use Sylius\Bundle\ApiBundle\Command\Account\VerifyCustomerAccount; use Sylius\Calendar\Provider\DateTimeProviderInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; use Sylius\Component\User\Model\UserInterface; +use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Stamp\DispatchAfterCurrentBusStamp; final class VerifyCustomerAccountHandlerSpec extends ObjectBehavior { function let( RepositoryInterface $shopUserRepository, DateTimeProviderInterface $dateTimeProvider, + MessageBusInterface $commandBus, ): void { - $this->beConstructedWith($shopUserRepository, $dateTimeProvider); + $this->beConstructedWith($shopUserRepository, $dateTimeProvider, $commandBus); } function it_is_a_message_handler(): void @@ -39,15 +44,22 @@ final class VerifyCustomerAccountHandlerSpec extends ObjectBehavior RepositoryInterface $shopUserRepository, DateTimeProviderInterface $dateTimeProvider, UserInterface $user, + MessageBusInterface $commandBus, ): void { $shopUserRepository->findOneBy(['emailVerificationToken' => 'ToKeN'])->willReturn($user); $dateTimeProvider->now()->willReturn(new \DateTime()); + $user->getEmail()->willReturn('shop@example.com'); $user->setVerifiedAt(Argument::type(\DateTime::class))->shouldBeCalled(); $user->setEmailVerificationToken(null)->shouldBeCalled(); $user->enable()->shouldBeCalled(); - $this(new VerifyCustomerAccount('ToKeN')); + $commandBus->dispatch( + new SendAccountRegistrationEmail('shop@example.com', 'en_US', 'WEB'), + [new DispatchAfterCurrentBusStamp()], + )->willReturn(new Envelope(new \stdClass())); + + $this(new VerifyCustomerAccount('ToKeN', 'en_US', 'WEB')); } function it_throws_error_if_user_does_not_exist( diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Catalog/AddProductReviewHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Catalog/AddProductReviewHandlerSpec.php index 02da3964c9..61be96ac04 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Catalog/AddProductReviewHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Catalog/AddProductReviewHandlerSpec.php @@ -15,6 +15,7 @@ namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Catalog; use PhpSpec\ObjectBehavior; use Sylius\Bundle\ApiBundle\Command\Catalog\AddProductReview; +use Sylius\Bundle\ApiBundle\Exception\ProductNotFoundException; use Sylius\Bundle\CoreBundle\Resolver\CustomerResolverInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ProductInterface; @@ -91,4 +92,21 @@ final class AddProductReviewHandlerSpec extends ObjectBehavior ]) ; } + + function it_throws_an_exception_if_product_has_not_been_found(ProductRepositoryInterface $productRepository): void + { + $productRepository->findOneByCode('winter_cap')->willReturn(null); + + $this + ->shouldThrow(ProductNotFoundException::class) + ->during('__invoke', [ + new AddProductReview( + 'Good stuff', + 5, + 'Really good stuff', + 'winter_cap', + ), + ]) + ; + } } diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ChoosePaymentMethodHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ChoosePaymentMethodHandlerSpec.php index e685be5779..d9e8d1b895 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ChoosePaymentMethodHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ChoosePaymentMethodHandlerSpec.php @@ -16,9 +16,11 @@ namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Checkout; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use SM\Factory\FactoryInterface; -use SM\StateMachine\StateMachineInterface; +use SM\StateMachine\StateMachineInterface as WinzouStateMachineInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\ApiBundle\Changer\PaymentMethodChangerInterface; use Sylius\Bundle\ApiBundle\Command\Checkout\ChoosePaymentMethod; +use Sylius\Bundle\ApiBundle\Exception\PaymentMethodCannotBeChangedException; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; @@ -54,7 +56,7 @@ final class ChoosePaymentMethodHandlerSpec extends ObjectBehavior OrderInterface $cart, PaymentInterface $payment, PaymentMethodInterface $paymentMethod, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $choosePaymentMethod = new ChoosePaymentMethod('CASH_ON_DELIVERY_METHOD'); $choosePaymentMethod->setOrderTokenValue('ORDERTOKEN'); @@ -81,6 +83,41 @@ final class ChoosePaymentMethodHandlerSpec extends ObjectBehavior $this($choosePaymentMethod)->shouldReturn($cart); } + function it_uses_the_new_state_machine_abstraction_if_passed( + OrderRepositoryInterface $orderRepository, + PaymentMethodRepositoryInterface $paymentMethodRepository, + PaymentRepositoryInterface $paymentRepository, + StateMachineInterface $stateMachine, + PaymentMethodChangerInterface $paymentMethodChanger, + OrderInterface $cart, + PaymentInterface $payment, + PaymentMethodInterface $paymentMethod, + ): void { + $this->beConstructedWith($orderRepository, $paymentMethodRepository, $paymentRepository, $stateMachine, $paymentMethodChanger); + + $choosePaymentMethod = new ChoosePaymentMethod('CASH_ON_DELIVERY_METHOD'); + $choosePaymentMethod->setOrderTokenValue('ORDERTOKEN'); + $choosePaymentMethod->setSubresourceId('123'); + + $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($cart); + $cart->getCheckoutState()->willReturn(OrderCheckoutStates::STATE_SHIPPING_SELECTED); + + $stateMachine->can($cart, OrderCheckoutTransitions::GRAPH, 'select_payment')->willReturn(true); + $stateMachine->apply($cart, OrderCheckoutTransitions::GRAPH, 'select_payment')->shouldBeCalled(); + + $paymentMethodRepository->findOneBy(['code' => 'CASH_ON_DELIVERY_METHOD'])->willReturn($paymentMethod); + + $cart->getId()->willReturn('111'); + + $paymentRepository->findOneByOrderId('123', '111')->willReturn($payment); + + $cart->getState()->willReturn(OrderInterface::STATE_CART); + + $payment->setMethod($paymentMethod)->shouldBeCalled(); + + $this($choosePaymentMethod)->shouldReturn($cart); + } + function it_throws_an_exception_if_order_with_given_token_has_not_been_found( OrderRepositoryInterface $orderRepository, PaymentInterface $payment, @@ -104,7 +141,7 @@ final class ChoosePaymentMethodHandlerSpec extends ObjectBehavior PaymentMethodRepositoryInterface $paymentMethodRepository, FactoryInterface $stateMachineFactory, OrderInterface $cart, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, PaymentInterface $payment, ): void { $choosePaymentMethod = new ChoosePaymentMethod('CASH_ON_DELIVERY_METHOD'); @@ -133,7 +170,7 @@ final class ChoosePaymentMethodHandlerSpec extends ObjectBehavior PaymentMethodRepositoryInterface $paymentMethodRepository, FactoryInterface $stateMachineFactory, OrderInterface $cart, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, PaymentInterface $payment, ): void { $choosePaymentMethod = new ChoosePaymentMethod('CASH_ON_DELIVERY_METHOD'); @@ -164,7 +201,7 @@ final class ChoosePaymentMethodHandlerSpec extends ObjectBehavior FactoryInterface $stateMachineFactory, OrderInterface $cart, PaymentMethodInterface $paymentMethod, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $choosePaymentMethod = new ChoosePaymentMethod('CASH_ON_DELIVERY_METHOD'); $choosePaymentMethod->setOrderTokenValue('ORDERTOKEN'); @@ -216,7 +253,7 @@ final class ChoosePaymentMethodHandlerSpec extends ObjectBehavior $payment->getState()->willReturn(PaymentInterface::STATE_CANCELLED); $this - ->shouldThrow(\InvalidArgumentException::class) + ->shouldThrow(PaymentMethodCannotBeChangedException::class) ->during('__invoke', [$choosePaymentMethod]) ; } diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ChooseShippingMethodHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ChooseShippingMethodHandlerSpec.php index 7e2f3665d8..2afd1a50f4 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ChooseShippingMethodHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ChooseShippingMethodHandlerSpec.php @@ -17,7 +17,8 @@ use Doctrine\Common\Collections\ArrayCollection; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use SM\Factory\FactoryInterface; -use SM\StateMachine\StateMachineInterface; +use SM\StateMachine\StateMachineInterface as WinzouStateMachineInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\ApiBundle\Command\Checkout\ChooseShippingMethod; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\ShipmentInterface; @@ -55,7 +56,7 @@ final class ChooseShippingMethodHandlerSpec extends ObjectBehavior OrderInterface $cart, ShippingMethodInterface $shippingMethod, ShipmentInterface $shipment, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $chooseShippingMethod = new ChooseShippingMethod('DHL_SHIPPING_METHOD'); $chooseShippingMethod->setOrderTokenValue('ORDERTOKEN'); @@ -82,6 +83,48 @@ final class ChooseShippingMethodHandlerSpec extends ObjectBehavior $this($chooseShippingMethod)->shouldReturn($cart); } + function it_uses_the_new_state_machine_abstraction_if_passed( + OrderRepositoryInterface $orderRepository, + ShippingMethodRepositoryInterface $shippingMethodRepository, + ShipmentRepositoryInterface $shipmentRepository, + ShippingMethodEligibilityCheckerInterface $eligibilityChecker, + StateMachineInterface $stateMachine, + OrderInterface $cart, + ShippingMethodInterface $shippingMethod, + ShipmentInterface $shipment, + ): void { + $this->beConstructedWith( + $orderRepository, + $shippingMethodRepository, + $shipmentRepository, + $eligibilityChecker, + $stateMachine, + ); + + $chooseShippingMethod = new ChooseShippingMethod('DHL_SHIPPING_METHOD'); + $chooseShippingMethod->setOrderTokenValue('ORDERTOKEN'); + $chooseShippingMethod->setSubresourceId('123'); + + $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($cart); + + $stateMachine->can($cart, OrderCheckoutTransitions::GRAPH, 'select_shipping')->willReturn(true); + + $shippingMethodRepository->findOneBy(['code' => 'DHL_SHIPPING_METHOD'])->willReturn($shippingMethod); + + $cart->getShipments()->willReturn(new ArrayCollection([$shipment->getWrappedObject()])); + + $cart->getId()->willReturn('111'); + + $shipmentRepository->findOneByOrderId('123', '111')->willReturn($shipment); + + $eligibilityChecker->isEligible($shipment, $shippingMethod)->willReturn(true); + + $shipment->setMethod($shippingMethod)->shouldBeCalled(); + $stateMachine->apply($cart, OrderCheckoutTransitions::GRAPH, 'select_shipping')->shouldBeCalled(); + + $this($chooseShippingMethod)->shouldReturn($cart); + } + function it_throws_an_exception_if_shipping_method_is_not_eligible( OrderRepositoryInterface $orderRepository, ShippingMethodRepositoryInterface $shippingMethodRepository, @@ -91,7 +134,7 @@ final class ChooseShippingMethodHandlerSpec extends ObjectBehavior OrderInterface $cart, ShippingMethodInterface $shippingMethod, ShipmentInterface $shipment, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $chooseShippingMethod = new ChooseShippingMethod('DHL_SHIPPING_METHOD'); $chooseShippingMethod->setOrderTokenValue('ORDERTOKEN'); @@ -143,7 +186,7 @@ final class ChooseShippingMethodHandlerSpec extends ObjectBehavior ShippingMethodRepositoryInterface $shippingMethodRepository, FactoryInterface $stateMachineFactory, OrderInterface $cart, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ShipmentInterface $shipment, ): void { $chooseShippingMethod = new ChooseShippingMethod('DHL_SHIPPING_METHOD'); @@ -169,7 +212,7 @@ final class ChooseShippingMethodHandlerSpec extends ObjectBehavior ShippingMethodRepositoryInterface $shippingMethodRepository, FactoryInterface $stateMachineFactory, OrderInterface $cart, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ShipmentInterface $shipment, ): void { $chooseShippingMethod = new ChooseShippingMethod('DHL_SHIPPING_METHOD'); @@ -199,7 +242,7 @@ final class ChooseShippingMethodHandlerSpec extends ObjectBehavior FactoryInterface $stateMachineFactory, OrderInterface $cart, ShippingMethodInterface $shippingMethod, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $chooseShippingMethod = new ChooseShippingMethod('DHL_SHIPPING_METHOD'); $chooseShippingMethod->setOrderTokenValue('ORDERTOKEN'); diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/CompleteOrderHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/CompleteOrderHandlerSpec.php index 43fbd94d03..c2016e7942 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/CompleteOrderHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/CompleteOrderHandlerSpec.php @@ -15,9 +15,11 @@ namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Checkout; use PhpSpec\ObjectBehavior; use SM\Factory\FactoryInterface; -use SM\StateMachine\StateMachineInterface; +use SM\StateMachine\StateMachineInterface as WinzouStateMachineInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\ApiBundle\Command\Cart\InformAboutCartRecalculation; use Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder; +use Sylius\Bundle\ApiBundle\CommandHandler\Checkout\Exception\OrderTotalHasChangedException; use Sylius\Bundle\ApiBundle\Event\OrderCompleted; use Sylius\Bundle\CoreBundle\Order\Checker\OrderPromotionsIntegrityCheckerInterface; use Sylius\Component\Core\Model\CustomerInterface; @@ -43,12 +45,12 @@ final class CompleteOrderHandlerSpec extends ObjectBehavior function it_handles_order_completion_without_notes( OrderRepositoryInterface $orderRepository, - StateMachineInterface $stateMachine, - OrderInterface $order, FactoryInterface $stateMachineFactory, MessageBusInterface $eventBus, - CustomerInterface $customer, OrderPromotionsIntegrityCheckerInterface $orderPromotionsIntegrityChecker, + WinzouStateMachineInterface $stateMachine, + OrderInterface $order, + CustomerInterface $customer, ): void { $completeOrder = new CompleteOrder(); $completeOrder->setOrderTokenValue('ORDERTOKEN'); @@ -56,6 +58,7 @@ final class CompleteOrderHandlerSpec extends ObjectBehavior $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order); $order->getCustomer()->willReturn($customer); + $order->getTotal()->willReturn(1500); $order->setNotes(null)->shouldNotBeCalled(); @@ -78,19 +81,59 @@ final class CompleteOrderHandlerSpec extends ObjectBehavior $this($completeOrder)->shouldReturn($order); } - function it_handles_order_completion_with_notes( + function it_uses_the_new_state_machine_abstraction_if_passed( OrderRepositoryInterface $orderRepository, StateMachineInterface $stateMachine, + MessageBusInterface $commandBus, + MessageBusInterface $eventBus, + OrderPromotionsIntegrityCheckerInterface $orderPromotionsIntegrityChecker, OrderInterface $order, + CustomerInterface $customer, + ): void { + $this->beConstructedWith($orderRepository, $stateMachine, $commandBus, $eventBus, $orderPromotionsIntegrityChecker); + + $completeOrder = new CompleteOrder(); + $completeOrder->setOrderTokenValue('ORDERTOKEN'); + + $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order); + + $order->getCustomer()->willReturn($customer); + $order->getTotal()->willReturn(1500); + + $order->setNotes(null)->shouldNotBeCalled(); + + $orderPromotionsIntegrityChecker->check($order)->willReturn(null); + + $stateMachine->can($order, OrderCheckoutTransitions::GRAPH, 'complete')->willReturn(true); + $order->getTokenValue()->willReturn('COMPLETED_ORDER_TOKEN'); + + $stateMachine->apply($order, OrderCheckoutTransitions::GRAPH, 'complete')->shouldBeCalled(); + + $orderCompleted = new OrderCompleted('COMPLETED_ORDER_TOKEN'); + + $eventBus + ->dispatch($orderCompleted, [new DispatchAfterCurrentBusStamp()]) + ->willReturn(new Envelope($orderCompleted)) + ->shouldBeCalled() + ; + + $this($completeOrder)->shouldReturn($order); + } + + function it_handles_order_completion_with_notes( + OrderRepositoryInterface $orderRepository, FactoryInterface $stateMachineFactory, MessageBusInterface $eventBus, - CustomerInterface $customer, OrderPromotionsIntegrityCheckerInterface $orderPromotionsIntegrityChecker, + WinzouStateMachineInterface $stateMachine, + OrderInterface $order, + CustomerInterface $customer, ): void { $completeOrder = new CompleteOrder('ThankYou'); $completeOrder->setOrderTokenValue('ORDERTOKEN'); $order->getCustomer()->willReturn($customer); + $order->getTotal()->willReturn(1500); $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order); @@ -117,16 +160,17 @@ final class CompleteOrderHandlerSpec extends ObjectBehavior function it_delays_an_information_about_cart_recalculate( OrderRepositoryInterface $orderRepository, - OrderInterface $order, MessageBusInterface $commandBus, + OrderPromotionsIntegrityCheckerInterface $orderPromotionsIntegrityChecker, + OrderInterface $order, CustomerInterface $customer, PromotionInterface $promotion, - OrderPromotionsIntegrityCheckerInterface $orderPromotionsIntegrityChecker, ): void { $completeOrder = new CompleteOrder('ThankYou'); $completeOrder->setOrderTokenValue('ORDERTOKEN'); $order->getCustomer()->willReturn($customer); + $order->getTotal()->willReturn(1000); $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order); @@ -160,13 +204,11 @@ final class CompleteOrderHandlerSpec extends ObjectBehavior ; } - function it_throws_an_exception_if_order_cannot_be_completed( + function it_throws_an_exception_if_order_total_has_changed( OrderRepositoryInterface $orderRepository, - StateMachineInterface $stateMachine, - OrderInterface $order, - FactoryInterface $stateMachineFactory, - CustomerInterface $customer, OrderPromotionsIntegrityCheckerInterface $orderPromotionsIntegrityChecker, + OrderInterface $order, + CustomerInterface $customer, ): void { $completeOrder = new CompleteOrder(); $completeOrder->setOrderTokenValue('ORDERTOKEN'); @@ -174,6 +216,31 @@ final class CompleteOrderHandlerSpec extends ObjectBehavior $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order); $order->getCustomer()->willReturn($customer); + $order->getTotal()->willReturn(1500, 2000); + + $orderPromotionsIntegrityChecker->check($order)->willReturn(null); + + $this + ->shouldThrow(OrderTotalHasChangedException::class) + ->during('__invoke', [$completeOrder]) + ; + } + + function it_throws_an_exception_if_order_cannot_be_completed( + OrderRepositoryInterface $orderRepository, + FactoryInterface $stateMachineFactory, + OrderPromotionsIntegrityCheckerInterface $orderPromotionsIntegrityChecker, + WinzouStateMachineInterface $stateMachine, + OrderInterface $order, + CustomerInterface $customer, + ): void { + $completeOrder = new CompleteOrder(); + $completeOrder->setOrderTokenValue('ORDERTOKEN'); + + $orderRepository->findOneBy(['tokenValue' => 'ORDERTOKEN'])->willReturn($order); + + $order->getCustomer()->willReturn($customer); + $order->getTotal()->willReturn(1500); $orderPromotionsIntegrityChecker->check($order)->willReturn(null); diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/SendOrderConfirmationHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/SendOrderConfirmationHandlerSpec.php index 8103c67d4e..3fd8afe414 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/SendOrderConfirmationHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/SendOrderConfirmationHandlerSpec.php @@ -15,19 +15,19 @@ namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Checkout; use PhpSpec\ObjectBehavior; use Sylius\Bundle\ApiBundle\Command\Checkout\SendOrderConfirmation; -use Sylius\Bundle\CoreBundle\Mailer\Emails; -use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Repository\OrderRepositoryInterface; -use Sylius\Component\Mailer\Sender\SenderInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; final class SendOrderConfirmationHandlerSpec extends ObjectBehavior { - function let(SenderInterface $sender, OrderRepositoryInterface $orderRepository): void - { - $this->beConstructedWith($sender, $orderRepository); + function let( + OrderRepositoryInterface $orderRepository, + OrderEmailManagerInterface $orderEmailManager, + ): void { + $this->beConstructedWith($orderRepository, $orderEmailManager); } function it_is_a_message_handler(): void @@ -36,32 +36,19 @@ final class SendOrderConfirmationHandlerSpec extends ObjectBehavior } function it_sends_order_confirmation_message( - OrderInterface $order, - SendOrderConfirmation $sendOrderConfirmation, - SenderInterface $sender, - CustomerInterface $customer, - ChannelInterface $channel, OrderRepositoryInterface $orderRepository, + OrderEmailManagerInterface $orderEmailManager, + OrderInterface $order, + CustomerInterface $customer, ): void { - $sendOrderConfirmation->orderToken()->willReturn('TOKEN'); - $orderRepository->findOneByTokenValue('TOKEN')->willReturn($order); - $order->getChannel()->willReturn($channel); $order->getLocaleCode()->willReturn('pl_PL'); $order->getCustomer()->willReturn($customer); $customer->getEmail()->willReturn('johnny.bravo@email.com'); - $sender->send( - Emails::ORDER_CONFIRMATION, - ['johnny.bravo@email.com'], - [ - 'order' => $order->getWrappedObject(), - 'channel' => $channel->getWrappedObject(), - 'localeCode' => 'pl_PL', - ], - )->shouldBeCalled(); + $orderEmailManager->sendConfirmationEmail($order)->shouldBeCalled(); $this(new SendOrderConfirmation('TOKEN')); } diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/SendShipmentConfirmationEmailHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/SendShipmentConfirmationEmailHandlerSpec.php index 437451ae48..2431c2a91e 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/SendShipmentConfirmationEmailHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/SendShipmentConfirmationEmailHandlerSpec.php @@ -15,20 +15,21 @@ namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Checkout; use PhpSpec\ObjectBehavior; use Sylius\Bundle\ApiBundle\Command\Checkout\SendShipmentConfirmationEmail; -use Sylius\Bundle\CoreBundle\Mailer\Emails; +use Sylius\Bundle\CoreBundle\Mailer\ShipmentEmailManagerInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\ShipmentInterface; use Sylius\Component\Core\Repository\ShipmentRepositoryInterface; -use Sylius\Component\Mailer\Sender\SenderInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; final class SendShipmentConfirmationEmailHandlerSpec extends ObjectBehavior { - function let(SenderInterface $sender, ShipmentRepositoryInterface $shipmentRepository): void - { - $this->beConstructedWith($sender, $shipmentRepository); + function let( + ShipmentRepositoryInterface $shipmentRepository, + ShipmentEmailManagerInterface $shipmentEmailManager, + ): void { + $this->beConstructedWith($shipmentRepository, $shipmentEmailManager); } function it_is_a_message_handler(): void @@ -37,11 +38,11 @@ final class SendShipmentConfirmationEmailHandlerSpec extends ObjectBehavior } function it_sends_shipment_confirmation_message( + ShipmentRepositoryInterface $shipmentRepository, + ShipmentEmailManagerInterface $shipmentEmailManager, ShipmentInterface $shipment, - SenderInterface $sender, CustomerInterface $customer, ChannelInterface $channel, - ShipmentRepositoryInterface $shipmentRepository, OrderInterface $order, ): void { $shipmentRepository->find(123)->willReturn($shipment); @@ -53,16 +54,7 @@ final class SendShipmentConfirmationEmailHandlerSpec extends ObjectBehavior $order->getCustomer()->willReturn($customer); $customer->getEmail()->willReturn('johnny.bravo@email.com'); - $sender->send( - Emails::SHIPMENT_CONFIRMATION, - ['johnny.bravo@email.com'], - [ - 'shipment' => $shipment->getWrappedObject(), - 'order' => $order->getWrappedObject(), - 'channel' => $channel->getWrappedObject(), - 'localeCode' => 'pl_PL', - ], - )->shouldBeCalled(); + $shipmentEmailManager->sendConfirmationEmail($shipment)->shouldBeCalled(); $this(new SendShipmentConfirmationEmail(123)); } diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ShipShipmentHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ShipShipmentHandlerSpec.php index c4298741db..c6ec85b3ba 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ShipShipmentHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Checkout/ShipShipmentHandlerSpec.php @@ -15,7 +15,8 @@ namespace spec\Sylius\Bundle\ApiBundle\CommandHandler\Checkout; use PhpSpec\ObjectBehavior; use SM\Factory\FactoryInterface; -use SM\StateMachine\StateMachineInterface; +use SM\StateMachine\StateMachineInterface as WinzouStateMachineInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\ApiBundle\Command\Checkout\SendShipmentConfirmationEmail; use Sylius\Bundle\ApiBundle\Command\Checkout\ShipShipment; use Sylius\Component\Core\Model\ShipmentInterface; @@ -37,7 +38,7 @@ final class ShipShipmentHandlerSpec extends ObjectBehavior function it_handles_shipping_without_tracking_number( ShipmentRepositoryInterface $shipmentRepository, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ShipmentInterface $shipment, FactoryInterface $stateMachineFactory, MessageBusInterface $eventBus, @@ -67,7 +68,7 @@ final class ShipShipmentHandlerSpec extends ObjectBehavior function it_handles_shipping_with_tracking_number( ShipmentRepositoryInterface $shipmentRepository, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ShipmentInterface $shipment, FactoryInterface $stateMachineFactory, MessageBusInterface $eventBus, @@ -95,6 +96,35 @@ final class ShipShipmentHandlerSpec extends ObjectBehavior $this($shipShipment)->shouldReturn($shipment); } + function it_uses_the_new_state_machine_abstraction_if_passed( + ShipmentRepositoryInterface $shipmentRepository, + StateMachineInterface $stateMachine, + MessageBusInterface $eventBus, + ShipmentInterface $shipment, + ): void { + $this->beConstructedWith($shipmentRepository, $stateMachine, $eventBus); + + $shipShipment = new ShipShipment('TRACK'); + $shipShipment->setShipmentId(123); + + $shipmentRepository->find(123)->willReturn($shipment); + + $shipment->setTracking('TRACK')->shouldBeCalled(); + + $stateMachine->can($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_SHIP)->willReturn(true); + $stateMachine->apply($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_SHIP)->shouldBeCalled(); + + $sendShipmentConfirmationEmail = new SendShipmentConfirmationEmail(123); + + $eventBus + ->dispatch($sendShipmentConfirmationEmail, [new DispatchAfterCurrentBusStamp()]) + ->willReturn(new Envelope($sendShipmentConfirmationEmail)) + ->shouldBeCalled() + ; + + $this($shipShipment)->shouldReturn($shipment); + } + function it_throws_an_exception_if_shipment_does_not_exist( ShipmentRepositoryInterface $shipmentRepository, ): void { @@ -111,7 +141,7 @@ final class ShipShipmentHandlerSpec extends ObjectBehavior function it_throws_an_exception_if_shipment_cannot_be_shipped( ShipmentRepositoryInterface $shipmentRepository, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ShipmentInterface $shipment, FactoryInterface $stateMachineFactory, ): void { diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Customer/RemoveShopUserHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Customer/RemoveShopUserHandlerSpec.php new file mode 100644 index 0000000000..080bb27656 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Customer/RemoveShopUserHandlerSpec.php @@ -0,0 +1,44 @@ +beConstructedWith($userRepository); + } + + public function it_throws_an_exception_if_user_has_not_been_found(UserRepositoryInterface $userRepository): void + { + $userRepository->find(42)->willReturn(null); + + $this->shouldThrow(UserNotFoundException::class)->during('__invoke', [new RemoveShopUser(42)]); + } + + public function it_should_remove_shop_user(UserRepositoryInterface $userRepository, ShopUserInterface $shopUser): void + { + $userRepository->find(42)->willReturn($shopUser); + $shopUser->setCustomer(null)->shouldBeCalled(); + $userRepository->remove($shopUser)->shouldBeCalled(); + + $this->__invoke(new RemoveShopUser(42)); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Promotion/GeneratePromotionCouponHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Promotion/GeneratePromotionCouponHandlerSpec.php new file mode 100644 index 0000000000..fc3963713e --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/Promotion/GeneratePromotionCouponHandlerSpec.php @@ -0,0 +1,63 @@ +beConstructedWith( + $promotionRepository, + $promotionCouponGenerator, + ); + } + + function it_throws_exception_if_promotion_is_not_found( + PromotionRepositoryInterface $promotionRepository, + ): void { + $promotionRepository->findOneBy(['code' => 'promotion_code'])->willReturn(null); + + $generatePromotionCoupon = new GeneratePromotionCoupon('promotion_code'); + + $this->shouldThrow(PromotionNotFoundException::class) + ->during('__invoke', [$generatePromotionCoupon]) + ; + } + + function it_generates_promotion_coupons( + PromotionRepositoryInterface $promotionRepository, + PromotionCouponGeneratorInterface $promotionCouponGenerator, + PromotionInterface $promotion, + PromotionCouponInterface $promotionCouponOne, + PromotionCouponInterface $promotionCouponTwo, + ): void { + $promotionRepository->findOneBy(['code' => 'promotion_code'])->willReturn($promotion); + + $generatePromotionCoupon = new GeneratePromotionCoupon('promotion_code'); + + $promotionCouponGenerator->generate($promotion, $generatePromotionCoupon)->willReturn([$promotionCouponOne, $promotionCouponTwo]); + + $this($generatePromotionCoupon)->shouldIterateAs([$promotionCouponOne, $promotionCouponTwo]); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/SendContactRequestHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/SendContactRequestHandlerSpec.php index acf4fdc30b..82a894314f 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/SendContactRequestHandlerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/CommandHandler/SendContactRequestHandlerSpec.php @@ -15,22 +15,24 @@ namespace spec\Sylius\Bundle\ApiBundle\CommandHandler; use PhpSpec\ObjectBehavior; use Sylius\Bundle\ApiBundle\Command\SendContactRequest; -use Sylius\Bundle\CoreBundle\Mailer\Emails; +use Sylius\Bundle\ApiBundle\Exception\ChannelNotFoundException; +use Sylius\Bundle\CoreBundle\Mailer\ContactEmailManagerInterface; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; use Sylius\Component\Core\Model\ChannelInterface; -use Sylius\Component\Mailer\Sender\SenderInterface; final class SendContactRequestHandlerSpec extends ObjectBehavior { - function let(SenderInterface $sender, ChannelRepositoryInterface $channelRepository): void - { - $this->beConstructedWith($sender, $channelRepository); + function let( + ChannelRepositoryInterface $channelRepository, + ContactEmailManagerInterface $contactEmailManager, + ): void { + $this->beConstructedWith($channelRepository, $contactEmailManager); } function it_sends_contact_request( - ChannelInterface $channel, ChannelRepositoryInterface $channelRepository, - SenderInterface $sender, + ContactEmailManagerInterface $contactEmailManager, + ChannelInterface $channel, ): void { $command = new SendContactRequest('adam@sylius.com', 'message'); $command->setChannelCode('CODE'); @@ -40,16 +42,11 @@ final class SendContactRequestHandlerSpec extends ObjectBehavior $channel->getContactEmail()->willReturn('channel@contact.com'); - $sender->send( - Emails::CONTACT_REQUEST, + $contactEmailManager->sendContactRequest( + ['message' => 'message', 'email' => 'adam@sylius.com'], ['channel@contact.com'], - [ - 'data' => ['message' => 'message', 'email' => 'adam@sylius.com'], - 'channel' => $channel, - 'localeCode' => 'en_US', - ], - [], - ['adam@sylius.com'], + $channel, + 'en_US', ); $this($command); @@ -63,7 +60,7 @@ final class SendContactRequestHandlerSpec extends ObjectBehavior $channelRepository->findOneByCode('CODE')->willReturn(null); $this - ->shouldThrow(\InvalidArgumentException::class) + ->shouldThrow(ChannelNotFoundException::class) ->during('__invoke', [$command]) ; } diff --git a/src/Sylius/Bundle/ApiBundle/spec/Creator/ProductImageCreatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Creator/ProductImageCreatorSpec.php new file mode 100644 index 0000000000..4bdb88eb48 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Creator/ProductImageCreatorSpec.php @@ -0,0 +1,135 @@ +beConstructedWith($productImageFactory, $productRepository, $imageUploader, $iriConverter); + } + + function it_creates_a_product_image( + FactoryInterface $productImageFactory, + ProductRepositoryInterface $productRepository, + ImageUploaderInterface $imageUploader, + IriConverterInterface $iriConverter, + ProductInterface $product, + ProductImageInterface $productImage, + ProductVariantInterface $productVariant, + ): void { + $file = new \SplFileInfo(__FILE__); + + $productRepository->findOneBy(['code' => 'CODE'])->willReturn($product); + $iriConverter->getResourceFromIri('/api/v2/product-variants/CODE')->willReturn($productVariant); + + $productImageFactory->createNew()->willReturn($productImage); + $productImage->setFile($file)->shouldBeCalled(); + $productImage->setType('banner')->shouldBeCalled(); + $productImage->addProductVariant($productVariant)->shouldBeCalled(); + + $product->addImage($productImage)->shouldBeCalled(); + + $imageUploader->upload($productImage)->shouldBeCalled(); + + $this + ->create( + 'CODE', + $file, + 'banner', + ['productVariants' => ['/api/v2/product-variants/CODE']], + ) + ->shouldReturn($productImage) + ; + } + + function it_throws_an_exception_if_product_is_not_found( + FactoryInterface $productImageFactory, + ProductRepositoryInterface $productRepository, + ImageUploaderInterface $imageUploader, + IriConverterInterface $iriConverter, + ): void { + $file = new \SplFileInfo(__FILE__); + + $productRepository->findOneBy(['code' => 'CODE'])->willReturn(null); + + $productImageFactory->createNew()->shouldNotBeCalled(); + $iriConverter->getResourceFromIri(Argument::any())->shouldNotBeCalled(); + $imageUploader->upload(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(ProductNotFoundException::class) + ->during('create', ['CODE', $file, 'banner', []]) + ; + } + + function it_throws_an_exception_if_there_is_no_uploaded_file( + FactoryInterface $productImageFactory, + ProductRepositoryInterface $productRepository, + ImageUploaderInterface $imageUploader, + IriConverterInterface $iriConverter, + ): void { + $productRepository->findOneBy(['code' => 'CODE'])->shouldNotBeCalled(); + $productImageFactory->createNew()->shouldNotBeCalled(); + $iriConverter->getResourceFromIri(Argument::any())->shouldNotBeCalled(); + $imageUploader->upload(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(NoFileUploadedException::class) + ->during('create', ['CODE', null, 'banner', []]) + ; + } + + function it_throws_an_exception_if_there_is_an_iri_to_different_resource_than_product_variant( + FactoryInterface $productImageFactory, + ProductRepositoryInterface $productRepository, + ImageUploaderInterface $imageUploader, + IriConverterInterface $iriConverter, + ProductInterface $product, + ProductImageInterface $productImage, + ): void { + $file = new \SplFileInfo(__FILE__); + + $productRepository->findOneBy(['code' => 'CODE'])->willReturn($product); + $iriConverter->getResourceFromIri('/api/v2/products/CODE')->willReturn($product); + + $productImageFactory->createNew()->willReturn($productImage); + $productImage->setFile($file)->shouldBeCalled(); + $productImage->setType('banner')->shouldBeCalled(); + + $productImage->addProductVariant(Argument::any())->shouldNotBeCalled(); + $imageUploader->upload(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('create', ['CODE', $file, 'banner', ['productVariants' => ['/api/v2/products/CODE']]]) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Creator/TaxonImageCreatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Creator/TaxonImageCreatorSpec.php new file mode 100644 index 0000000000..e446b700c9 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Creator/TaxonImageCreatorSpec.php @@ -0,0 +1,90 @@ +beConstructedWith($taxonImageFactory, $taxonRepository, $imageUploader); + } + + function it_creates_taxon_image( + FactoryInterface $taxonImageFactory, + TaxonRepositoryInterface $taxonRepository, + ImageUploaderInterface $imageUploader, + TaxonInterface $taxon, + TaxonImageInterface $taxonImage, + ): void { + $file = new \SplFileInfo(__FILE__); + + $taxonRepository->findOneBy(['code' => 'CODE'])->willReturn($taxon); + + $taxonImageFactory->createNew()->willReturn($taxonImage); + $taxonImage->setFile($file)->shouldBeCalled(); + $taxonImage->setType('banner')->shouldBeCalled(); + + $taxon->addImage($taxonImage)->shouldBeCalled(); + + $imageUploader->upload($taxonImage)->shouldBeCalled(); + + $this->create('CODE', $file, 'banner')->shouldReturn($taxonImage); + } + + function it_throws_an_exception_if_taxon_is_not_found( + FactoryInterface $taxonImageFactory, + TaxonRepositoryInterface $taxonRepository, + ImageUploaderInterface $imageUploader, + ): void { + $file = new \SplFileInfo(__FILE__); + + $taxonRepository->findOneBy(['code' => 'CODE'])->willReturn(null); + + $taxonImageFactory->createNew()->shouldNotBeCalled(); + $imageUploader->upload(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(TaxonNotFoundException::class) + ->during('create', ['CODE', $file, 'banner']) + ; + } + + function it_throws_an_exception_if_there_is_no_uploaded_file( + FactoryInterface $taxonImageFactory, + TaxonRepositoryInterface $taxonRepository, + ImageUploaderInterface $imageUploader, + ): void { + $taxonRepository->findOneBy(['code' => 'CODE'])->shouldNotBeCalled(); + $taxonImageFactory->createNew()->shouldNotBeCalled(); + $imageUploader->upload(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(NoFileUploadedException::class) + ->during('create', ['CODE', null, 'banner']) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ChannelDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ChannelDataPersisterSpec.php new file mode 100644 index 0000000000..e0750d5f78 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ChannelDataPersisterSpec.php @@ -0,0 +1,68 @@ +beConstructedWith($decoratedDataPersister, $channelDeletionChecker); + } + + function it_supports_only_channel_entity(ChannelInterface $channel, \stdClass $object): void + { + $this->supports($channel)->shouldReturn(true); + $this->supports($object)->shouldReturn(false); + } + + function it_persists_channel( + ContextAwareDataPersisterInterface $decoratedDataPersister, + ChannelInterface $channel, + ): void { + $decoratedDataPersister->persist($channel, [])->willReturn($channel); + + $this->persist($channel, [])->shouldReturn($channel); + } + + function it_throws_an_exception_if_channel_is_not_deletable( + ChannelDeletionCheckerInterface $channelDeletionChecker, + ChannelInterface $channel, + ): void { + $channelDeletionChecker->isDeletable($channel)->willReturn(false); + + $this + ->shouldThrow(ChannelCannotBeRemoved::class) + ->during('remove', [$channel]) + ; + } + + function it_removes_channel( + ContextAwareDataPersisterInterface $decoratedDataPersister, + ChannelDeletionCheckerInterface $channelDeletionChecker, + ChannelInterface $channel, + ): void { + $channelDeletionChecker->isDeletable($channel)->willReturn(true); + $decoratedDataPersister->remove($channel, [])->willReturn(null); + + $this->remove($channel, [])->shouldReturn(null); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/CustomerDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/CustomerDataPersisterSpec.php new file mode 100644 index 0000000000..e15815950f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/CustomerDataPersisterSpec.php @@ -0,0 +1,90 @@ +beConstructedWith($decoratedDataPersister, $passwordUpdater); + } + + function it_supports_only_customer_entity(CustomerInterface $customer, ProductInterface $product): void + { + $this->supports($customer)->shouldReturn(true); + $this->supports($product)->shouldReturn(false); + } + + function it_does_not_update_password_when_user_is_null( + ContextAwareDataPersisterInterface $decoratedDataPersister, + PasswordUpdaterInterface $passwordUpdater, + CustomerInterface $customer, + ): void { + $customer->getUser()->willReturn(null); + $passwordUpdater->updatePassword(Argument::any())->shouldNotBeCalled(); + + $decoratedDataPersister->persist($customer, [])->shouldBeCalled(); + + $this->persist($customer, []); + } + + function it_does_not_update_password_when_plain_password_is_null( + ContextAwareDataPersisterInterface $decoratedDataPersister, + PasswordUpdaterInterface $passwordUpdater, + CustomerInterface $customer, + ShopUserInterface $user, + ): void { + $user->getPlainPassword()->willReturn(null); + $customer->getUser()->willReturn($user); + $passwordUpdater->updatePassword($user)->shouldNotBeCalled(); + + $decoratedDataPersister->persist($customer, [])->shouldBeCalled(); + + $this->persist($customer, []); + } + + function it_updates_password_when_plain_password_is_set( + ContextAwareDataPersisterInterface $decoratedDataPersister, + PasswordUpdaterInterface $passwordUpdater, + CustomerInterface $customer, + ShopUserInterface $user, + ): void { + $user->getPlainPassword()->willReturn('password'); + $customer->getUser()->willReturn($user); + $passwordUpdater->updatePassword($user)->shouldBeCalled(); + + $decoratedDataPersister->persist($customer, [])->shouldBeCalled(); + + $this->persist($customer, []); + } + + function it_uses_decorated_data_persister_to_remove_customer( + ContextAwareDataPersisterInterface $decoratedDataPersister, + CustomerInterface $customer, + ): void { + $decoratedDataPersister->remove($customer, [])->shouldBeCalled(); + + $this->remove($customer, []); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/LocaleDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/LocaleDataPersisterSpec.php new file mode 100644 index 0000000000..70099d34b1 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/LocaleDataPersisterSpec.php @@ -0,0 +1,77 @@ +beConstructedWith($decoratedDataPersister, $localeUsageChecker); + } + + public function it_supports_only_locale_interface(): void + { + $this->supports(new stdClass())->shouldReturn(false); + $this->supports(new Locale())->shouldReturn(true); + } + + public function it_persists_locale( + ContextAwareDataPersisterInterface $decoratedDataPersister, + Locale $locale, + ): void { + $decoratedDataPersister->persist($locale, [])->shouldBeCalled()->willReturn(new stdClass()); + + $this->persist($locale); + } + + public function it_removes_locale( + ContextAwareDataPersisterInterface $decoratedDataPersister, + LocaleUsageCheckerInterface $localeUsageChecker, + Locale $locale, + ): void { + $locale->getCode()->willReturn('en_US'); + + $localeUsageChecker->isUsed('en_US')->willReturn(false); + + $decoratedDataPersister->remove($locale, [])->shouldBeCalled(); + + $this->remove($locale); + } + + public function it_throws_an_exception_if_locale_is_used( + ContextAwareDataPersisterInterface $decoratedDataPersister, + LocaleUsageCheckerInterface $localeUsageChecker, + Locale $locale, + ): void { + $locale->getCode()->willReturn('en_US'); + + $localeUsageChecker->isUsed('en_US')->willReturn(true); + + $decoratedDataPersister->remove($locale, [])->shouldNotBeCalled(); + + $this + ->shouldThrow(LocaleIsUsedException::class) + ->during('remove', [$locale]) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PaymentMethodDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PaymentMethodDataPersisterSpec.php new file mode 100644 index 0000000000..cfad0fbe91 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PaymentMethodDataPersisterSpec.php @@ -0,0 +1,66 @@ +beConstructedWith($dataPersister); + } + + function it_is_a_context_aware_persister(): void + { + $this->shouldImplement(ContextAwareDataPersisterInterface::class); + } + + function it_supports_only_payment_method(PaymentMethodInterface $paymentMethod): void + { + $this->supports(new \stdClass())->shouldReturn(false); + $this->supports($paymentMethod)->shouldReturn(true); + } + + function it_uses_inner_persister_to_persist_payment_method( + ContextAwareDataPersisterInterface $dataPersister, + PaymentMethodInterface $paymentMethod, + ): void { + $dataPersister->persist($paymentMethod, [])->shouldBeCalled(); + + $this->persist($paymentMethod); + } + + function it_throws_cannot_be_removed_exception_if_constraint_fails_on_removal( + ContextAwareDataPersisterInterface $dataPersister, + PaymentMethodInterface $paymentMethod, + ): void { + $dataPersister->remove($paymentMethod, [])->willThrow(ForeignKeyConstraintViolationException::class); + + $this->shouldThrow(PaymentMethodCannotBeRemoved::class)->during('remove', [$paymentMethod]); + } + + function it_uses_inner_persister_to_remove_payment_method( + ContextAwareDataPersisterInterface $dataPersister, + PaymentMethodInterface $paymentMethod, + ): void { + $dataPersister->remove($paymentMethod, [])->shouldBeCalled(); + + $this->remove($paymentMethod); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductAttributeDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductAttributeDataPersisterSpec.php new file mode 100644 index 0000000000..b9761c9362 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductAttributeDataPersisterSpec.php @@ -0,0 +1,72 @@ +beConstructedWith($persister); + } + + function it_is_a_context_aware_persister(): void + { + $this->shouldImplement(ContextAwareDataPersisterInterface::class); + } + + function it_supports_only_product_attribute(ProductAttributeInterface $productAttribute): void + { + $this->supports(new \stdClass())->shouldReturn(false); + $this->supports($productAttribute)->shouldReturn(true); + } + + function it_uses_inner_persister_to_persist_product_attribute( + ContextAwareDataPersisterInterface $persister, + ProductAttributeInterface $productAttribute, + ): void { + $persister->persist($productAttribute, [])->shouldBeCalled(); + + $this->persist($productAttribute, []); + } + + function it_throws_cannot_be_removed_exception_if_constraint_fails_on_removal( + ContextAwareDataPersisterInterface $persister, + ProductAttributeInterface $productAttribute, + ): void { + $persister + ->remove($productAttribute, []) + ->willThrow(ForeignKeyConstraintViolationException::class) + ; + + $this + ->shouldThrow(ProductAttributeCannotBeRemoved::class) + ->during('remove', [$productAttribute]) + ; + } + + function it_uses_inner_persister_to_remove_product_attribute( + ContextAwareDataPersisterInterface $persister, + ProductAttributeInterface $productAttribute, + ): void { + $persister->remove($productAttribute, [])->shouldBeCalled(); + + $this->remove($productAttribute, []); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductDataPersisterSpec.php new file mode 100644 index 0000000000..f2bbea3eae --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductDataPersisterSpec.php @@ -0,0 +1,72 @@ +beConstructedWith($persister); + } + + function it_is_a_context_aware_persister(): void + { + $this->shouldImplement(ContextAwareDataPersisterInterface::class); + } + + function it_supports_only_product(ProductInterface $product): void + { + $this->supports(new \stdClass())->shouldReturn(false); + $this->supports($product)->shouldReturn(true); + } + + function it_uses_inner_persister_to_persist_a_product( + ContextAwareDataPersisterInterface $persister, + ProductInterface $product, + ): void { + $persister->persist($product, [])->shouldBeCalled(); + + $this->persist($product, []); + } + + function it_throws_cannot_be_removed_exception_if_constraint_fails_on_removal( + ContextAwareDataPersisterInterface $persister, + ProductInterface $product, + ): void { + $persister + ->remove($product, []) + ->willThrow(ForeignKeyConstraintViolationException::class) + ; + + $this + ->shouldThrow(ProductCannotBeRemoved::class) + ->during('remove', [$product]) + ; + } + + function it_uses_inner_persister_to_remove_a_product( + ContextAwareDataPersisterInterface $persister, + ProductInterface $product, + ): void { + $persister->remove($product, [])->shouldBeCalled(); + + $this->remove($product, []); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductTaxonDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductTaxonDataPersisterSpec.php new file mode 100644 index 0000000000..1b42b121e1 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductTaxonDataPersisterSpec.php @@ -0,0 +1,71 @@ +beConstructedWith($decoratedDataPersister, $eventBus); + } + + function it_supports_only_product_taxon_entity(ProductTaxonInterface $productTaxon, ProductInterface $product): void + { + $this->supports($productTaxon)->shouldReturn(true); + $this->supports($product)->shouldReturn(false); + } + + function it_uses_decorated_data_persister_to_remove_product_taxon( + ContextAwareDataPersisterInterface $decoratedDataPersister, + ProductTaxonInterface $productTaxon, + ProductInterface $product, + MessageBusInterface $eventBus, + ): void { + $productTaxon->getProduct()->willReturn($product); + $product->getCode()->willReturn('t_shirt'); + $message = new ProductUpdated('t_shirt'); + + $decoratedDataPersister->remove($productTaxon, [])->shouldBeCalled(); + $eventBus->dispatch($message)->willReturn(new Envelope($message))->shouldBeCalled(); + + $this->remove($productTaxon, []); + } + + function it_uses_decorated_data_persister_to_persist_product_taxon_and_product( + ContextAwareDataPersisterInterface $decoratedDataPersister, + ProductTaxonInterface $productTaxon, + ProductInterface $product, + MessageBusInterface $eventBus, + ): void { + $productTaxon->getProduct()->willReturn($product); + $product->getCode()->willReturn('t_shirt'); + $message = new ProductUpdated('t_shirt'); + + $product->addProductTaxon($productTaxon)->shouldBeCalled(); + $decoratedDataPersister->persist($productTaxon, [])->shouldBeCalled(); + $eventBus->dispatch($message)->willReturn(new Envelope($message))->shouldBeCalled(); + + $this->persist($productTaxon, []); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductVariantDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductVariantDataPersisterSpec.php new file mode 100644 index 0000000000..577df42f4f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/ProductVariantDataPersisterSpec.php @@ -0,0 +1,66 @@ +beConstructedWith($dataPersister); + } + + function it_is_a_context_aware_persister(): void + { + $this->shouldImplement(ContextAwareDataPersisterInterface::class); + } + + function it_supports_only_product_variant(ProductVariantInterface $productVariant): void + { + $this->supports(new \stdClass())->shouldReturn(false); + $this->supports($productVariant)->shouldReturn(true); + } + + function it_uses_inner_persister_to_persist_product_variant( + ContextAwareDataPersisterInterface $dataPersister, + ProductVariantInterface $productVariant, + ): void { + $dataPersister->persist($productVariant, [])->shouldBeCalled(); + + $this->persist($productVariant); + } + + function it_throws_cannot_be_removed_exception_if_constraint_fails_on_removal( + ContextAwareDataPersisterInterface $dataPersister, + ProductVariantInterface $productVariant, + ): void { + $dataPersister->remove($productVariant, [])->willThrow(ForeignKeyConstraintViolationException::class); + + $this->shouldThrow(ProductVariantCannotBeRemoved::class)->during('remove', [$productVariant]); + } + + function it_uses_inner_persister_to_remove_product_variant( + ContextAwareDataPersisterInterface $dataPersister, + ProductVariantInterface $productVariant, + ): void { + $dataPersister->remove($productVariant, [])->shouldBeCalled(); + + $this->remove($productVariant); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PromotionCouponDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PromotionCouponDataPersisterSpec.php new file mode 100644 index 0000000000..d1e7742ad0 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PromotionCouponDataPersisterSpec.php @@ -0,0 +1,66 @@ +beConstructedWith($dataPersister); + } + + function it_is_a_context_aware_persister(): void + { + $this->shouldImplement(ContextAwareDataPersisterInterface::class); + } + + function it_supports_only_promotion_coupon(PromotionCouponInterface $coupon): void + { + $this->supports(new \stdClass())->shouldReturn(false); + $this->supports($coupon)->shouldReturn(true); + } + + function it_uses_inner_persister_to_persist_promotion_coupon( + ContextAwareDataPersisterInterface $dataPersister, + PromotionCouponInterface $coupon, + ): void { + $dataPersister->persist($coupon, [])->shouldBeCalled(); + + $this->persist($coupon); + } + + function it_throws_cannot_be_removed_exception_if_constraint_fails_on_removal( + ContextAwareDataPersisterInterface $dataPersister, + PromotionCouponInterface $coupon, + ): void { + $dataPersister->remove($coupon, [])->willThrow(ForeignKeyConstraintViolationException::class); + + $this->shouldThrow(PromotionCouponCannotBeRemoved::class)->during('remove', [$coupon]); + } + + function it_uses_inner_persister_to_remove_promotion_coupon( + ContextAwareDataPersisterInterface $dataPersister, + PromotionCouponInterface $coupon, + ): void { + $dataPersister->remove($coupon, [])->shouldBeCalled(); + + $this->remove($coupon); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PromotionDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PromotionDataPersisterSpec.php new file mode 100644 index 0000000000..c530e90678 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/PromotionDataPersisterSpec.php @@ -0,0 +1,66 @@ +beConstructedWith($dataPersister); + } + + function it_is_a_context_aware_persister(): void + { + $this->shouldImplement(ContextAwareDataPersisterInterface::class); + } + + function it_supports_only_promotion(PromotionInterface $promotion): void + { + $this->supports(new \stdClass())->shouldReturn(false); + $this->supports($promotion)->shouldReturn(true); + } + + function it_uses_inner_persister_to_persist_promotion( + ContextAwareDataPersisterInterface $dataPersister, + PromotionInterface $promotion, + ): void { + $dataPersister->persist($promotion, [])->shouldBeCalled(); + + $this->persist($promotion); + } + + function it_throws_cannot_be_removed_exception_if_constraint_fails_on_removal( + ContextAwareDataPersisterInterface $dataPersister, + PromotionInterface $promotion, + ): void { + $dataPersister->remove($promotion, [])->willThrow(ForeignKeyConstraintViolationException::class); + + $this->shouldThrow(PromotionCannotBeRemoved::class)->during('remove', [$promotion]); + } + + function it_uses_inner_persister_to_remove_promotion( + ContextAwareDataPersisterInterface $dataPersister, + PromotionInterface $promotion, + ): void { + $dataPersister->remove($promotion, [])->shouldBeCalled(); + + $this->remove($promotion); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataPersister/TranslatableDataPersisterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/TranslatableDataPersisterSpec.php new file mode 100644 index 0000000000..2c903d3b4d --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataPersister/TranslatableDataPersisterSpec.php @@ -0,0 +1,78 @@ +beConstructedWith($localeProvider); + } + + function it_is_a_context_aware_data_persister(): void + { + $this->shouldImplement(ContextAwareDataPersisterInterface::class); + } + + function it_is_a_resumable_data_persister(): void + { + $this->shouldImplement(ResumableDataPersisterInterface::class); + } + + function it_supports_only_translatable(TranslatableInterface $translatable): void + { + $this->supports(new \stdClass())->shouldReturn(false); + $this->supports($translatable)->shouldReturn(true); + } + + function it_does_nothing_if_there_is_a_translation_in_default_locale( + TranslationLocaleProviderInterface $localeProvider, + TranslatableInterface $translatable, + TranslatableInterface $translation, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en_US'); + $translatable->getTranslations()->willReturn(new ArrayCollection(['en_US' => $translation])); + + $this->persist($translatable)->shouldReturn($translatable); + } + + function it_throws_an_exception_if_there_is_no_translation_in_default_locale( + TranslationLocaleProviderInterface $localeProvider, + TranslatableInterface $translatable, + TranslatableInterface $translation, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en_US'); + $translatable->getTranslations()->willReturn(new ArrayCollection(['de_DE' => $translation])); + + $this->shouldThrow(TranslationInDefaultLocaleCannotBeRemoved::class)->during('persist', [$translatable]); + } + + function it_does_nothing_during_removing_object(TranslatableInterface $translatable): void + { + $this->remove($translatable)->shouldReturn($translatable); + } + + function it_is_resumable(): void + { + $this->resumable()->shouldReturn(true); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataProvider/AdminOrderItemAdjustmentsSubresourceDataProviderSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataProvider/AdminOrderItemAdjustmentsSubresourceDataProviderSpec.php new file mode 100644 index 0000000000..24df377ecf --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/DataProvider/AdminOrderItemAdjustmentsSubresourceDataProviderSpec.php @@ -0,0 +1,119 @@ +beConstructedWith($orderItemRepository, $sectionProvider); + } + + function it_does_not_support_not_adjustment_resource(): void + { + $this + ->supports(ProductInterface::class, Request::METHOD_GET, []) + ->shouldReturn(false) + ; + } + + function it_does_not_support_in_shop_api_section( + SectionProviderInterface $sectionProvider, + ShopApiSection $shopApiSection, + ): void { + $sectionProvider->getSection()->willReturn($shopApiSection); + + $this + ->supports(AdjustmentInterface::class, Request::METHOD_GET, []) + ->shouldReturn(false) + ; + } + + function it_does_not_support_not_order_item_subresource( + SectionProviderInterface $sectionProvider, + AdminApiSection $adminApiSection, + ): void { + $sectionProvider->getSection()->willReturn($adminApiSection); + + $context['subresource_resources'] = [ + ProductInterface::class => ['id' => 2], + ]; + + $this + ->supports(AdjustmentInterface::class, Request::METHOD_GET, $context) + ->shouldReturn(false) + ; + } + + function it_supports_order_item_adjustments_subresource_in_admin_api_section( + SectionProviderInterface $sectionProvider, + AdminApiSection $adminApiSection, + ): void { + $sectionProvider->getSection()->willReturn($adminApiSection); + $context['subresource_resources'] = [ + OrderItemInterface::class => ['id' => 2], + ]; + + $this + ->supports(AdjustmentInterface::class, Request::METHOD_GET, $context) + ->shouldReturn(true) + ; + } + + function it_providers_empty_array_if_order_item_does_not_exist( + OrderItemRepositoryInterface $orderItemRepository, + ): void { + $context['subresource_identifiers'] = ['id' => '11']; + $orderItemRepository->find('11')->willReturn(null); + + $this + ->getSubresource(AdjustmentInterface::class, [], $context, Request::METHOD_GET) + ->shouldReturn([]); + } + + function it_returns_order_item_adjustments( + OrderItemRepositoryInterface $orderItemRepository, + OrderItemInterface $orderItem, + AdjustmentInterface $adjustment1, + AdjustmentInterface $adjustment2, + ): void { + $context['subresource_identifiers'] = ['id' => '11']; + $orderItemRepository->find('11')->willReturn($orderItem); + $orderItem->getAdjustmentsRecursively()->willReturn( + new ArrayCollection([$adjustment1->getWrappedObject(), $adjustment2->getWrappedObject()]), + ); + + $this + ->getSubresource( + AdjustmentInterface::class, + [], + $context, + Request::METHOD_GET, + ) + ->shouldBeLike(new ArrayCollection([$adjustment1->getWrappedObject(), $adjustment2->getWrappedObject()])); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataProvider/OrderItemAdjustmentsSubresourceDataProviderSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataProvider/OrderItemAdjustmentsSubresourceDataProviderSpec.php index 643c977a0b..b5021f8d99 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/DataProvider/OrderItemAdjustmentsSubresourceDataProviderSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/DataProvider/OrderItemAdjustmentsSubresourceDataProviderSpec.php @@ -17,8 +17,8 @@ use Doctrine\Common\Collections\ArrayCollection; use PhpSpec\ObjectBehavior; use Sylius\Component\Core\Model\OrderItemInterface; use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Repository\OrderItemRepositoryInterface; use Sylius\Component\Order\Model\AdjustmentInterface; -use Sylius\Component\Order\Repository\OrderItemRepositoryInterface; use Symfony\Component\HttpFoundation\Request; final class OrderItemAdjustmentsSubresourceDataProviderSpec extends ObjectBehavior @@ -42,21 +42,15 @@ final class OrderItemAdjustmentsSubresourceDataProviderSpec extends ObjectBehavi ; } - function it_throws_an_exception_if_order_item_with_given_id_does_not_exist( + function it_providers_empty_array_if_order_item_does_not_exist( OrderItemRepositoryInterface $orderItemRepository, ): void { - $context['subresource_identifiers'] = ['tokenValue' => 'TOKEN', 'items' => 11]; - $orderItemRepository->find(11)->willReturn(null); + $context['subresource_identifiers'] = ['tokenValue' => 'TOKEN', 'items' => '11']; + $orderItemRepository->findOneByIdAndOrderTokenValue(11, 'TOKEN')->willReturn(null); $this - ->shouldThrow(\InvalidArgumentException::class) - ->during('getSubresource', [ - AdjustmentInterface::class, - [], - $context, - Request::METHOD_GET, - ]) - ; + ->getSubresource(AdjustmentInterface::class, [], $context, Request::METHOD_GET) + ->shouldReturn([]); } function it_returns_order_adjustments( @@ -64,8 +58,8 @@ final class OrderItemAdjustmentsSubresourceDataProviderSpec extends ObjectBehavi OrderItemInterface $orderItem, AdjustmentInterface $adjustment, ): void { - $context['subresource_identifiers'] = ['tokenValue' => 'TOKEN', 'items' => 11]; - $orderItemRepository->find(11)->willReturn($orderItem); + $context['subresource_identifiers'] = ['tokenValue' => 'TOKEN', 'items' => '11']; + $orderItemRepository->findOneByIdAndOrderTokenValue(11, 'TOKEN')->willReturn($orderItem); $orderItem->getAdjustmentsRecursively()->willReturn(new ArrayCollection([$adjustment->getWrappedObject()])); diff --git a/src/Sylius/Bundle/ApiBundle/spec/DataProvider/ProductItemDataProviderSpec.php b/src/Sylius/Bundle/ApiBundle/spec/DataProvider/ProductItemDataProviderSpec.php index 7e952a9d37..db9bbbee87 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/DataProvider/ProductItemDataProviderSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/DataProvider/ProductItemDataProviderSpec.php @@ -72,7 +72,7 @@ final class ProductItemDataProviderSpec extends ObjectBehavior $userContext->getUser()->willReturn($user); $user->getRoles()->willReturn([]); - $productRepository->findOneByChannelAndCode($channel, 'FORD_FOCUS')->willReturn($product); + $productRepository->findOneByChannelAndCodeWithAvailableAssociations($channel, 'FORD_FOCUS')->willReturn($product); $this ->getItem( @@ -95,7 +95,7 @@ final class ProductItemDataProviderSpec extends ObjectBehavior ): void { $userContext->getUser()->willReturn(null); - $productRepository->findOneByChannelAndCode($channel, 'FORD_FOCUS')->willReturn($product); + $productRepository->findOneByChannelAndCodeWithAvailableAssociations($channel, 'FORD_FOCUS')->willReturn($product); $this ->getItem( diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/AvailableProductAssociationsInProductCollectionExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/AvailableProductAssociationsInProductCollectionExtensionSpec.php new file mode 100644 index 0000000000..a25f15c43f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/AvailableProductAssociationsInProductCollectionExtensionSpec.php @@ -0,0 +1,102 @@ +beConstructedWith($userContext); + } + + public function it_is_a_constraint_validator() + { + $this->shouldHaveType(ContextAwareQueryCollectionExtensionInterface::class); + } + + function it_does_nothing_if_current_resource_is_not_a_product( + UserContextInterface $userContext, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $userContext->getUser()->shouldNotBeCalled(); + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + + $this->applyToCollection($queryBuilder, $queryNameGenerator, TaxonInterface::class, 'get', []); + } + + function it_does_nothing_if_current_user_is_an_admin_user( + UserContextInterface $userContext, + UserInterface $user, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $userContext->getUser()->willReturn($user); + $user->getRoles()->willReturn(['ROLE_API_ACCESS']); + + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + + $this->applyToCollection($queryBuilder, $queryNameGenerator, ProductInterface::class, 'get', []); + } + + function it_filters_products_by_available_associations( + UserContextInterface $userContext, + UserInterface $user, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ChannelInterface $channel, + Expr $expr, + Expr\Comparison $comparison, + Andx $andx, + ): void { + $userContext->getUser()->willReturn($user); + $user->getRoles()->willReturn([]); + + $queryNameGenerator->generateParameterName('enabled')->shouldBeCalled()->willReturn('enabled'); + $queryNameGenerator->generateJoinAlias('association')->shouldBeCalled()->willReturn('association'); + $queryNameGenerator->generateJoinAlias('associatedProduct')->shouldBeCalled()->willReturn('associatedProduct'); + + $queryBuilder->getRootAliases()->willReturn(['o']); + $queryBuilder->addSelect('o')->shouldBeCalled()->willReturn($queryBuilder); + + $queryBuilder->addSelect('association')->shouldBeCalled()->willReturn($queryBuilder); + $queryBuilder->leftJoin('o.associations', 'association')->shouldBeCalled()->willReturn($queryBuilder); + + $expr->andX(Argument::type(Expr\Comparison::class), Argument::type(Expr\Comparison::class))->willReturn($andx); + $expr->eq('associatedProduct.enabled', 'true')->shouldBeCalled()->willReturn($comparison); + $expr->eq('association.owner', 'o')->shouldBeCalled()->willReturn($comparison); + $queryBuilder->expr()->willReturn($expr->getWrappedObject()); + $queryBuilder->leftJoin('association.associatedProducts', 'associatedProduct', 'WITH', Argument::type(Andx::class))->shouldBeCalled()->willReturn($queryBuilder); + + $queryBuilder->andWhere('o.associations IS EMPTY OR associatedProduct.id IS NOT NULL')->shouldBeCalled()->willReturn($queryBuilder); + $queryBuilder->andWhere('o.enabled = :enabled')->shouldBeCalled()->willReturn($queryBuilder); + + $queryBuilder->setParameter('enabled', true)->shouldBeCalled()->willReturn($queryBuilder); + + $this->applyToCollection($queryBuilder, $queryNameGenerator, ProductInterface::class, 'get', []); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtensionSpec.php new file mode 100644 index 0000000000..aa2f8f25a6 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/HideArchivedPromotionExtensionSpec.php @@ -0,0 +1,61 @@ +beConstructedWith('promotionClass'); + } + + function it_does_nothing_if_current_resource_is_not_a_promotion( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + $queryBuilder->andWhere()->shouldNotBeCalled(); + + $this->applyToCollection($queryBuilder, $queryNameGenerator, 'taxonClass', 'get', []); + } + + function it_does_nothing_if_archived_at_filter_is_already_applied( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + $queryBuilder->andWhere()->shouldNotBeCalled(); + + $this->applyToCollection($queryBuilder, $queryNameGenerator, 'promotionClass', 'get', ['filters' => ['exists' => ['archivedAt' => 'true']]]); + } + + function it_filters_archived_promotions( + QueryBuilder $queryBuilder, + Expr $expr, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $queryBuilder->getRootAliases()->willReturn(['o']); + + $expr->isNull('o.archivedAt')->willReturn('o.archivedAt IS NULL'); + $queryBuilder->expr()->willReturn($expr); + $queryBuilder->andWhere('o.archivedAt IS NULL')->shouldBeCalled(); + + $this->applyToCollection($queryBuilder, $queryNameGenerator, 'promotionClass', 'get', []); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/ProductsByTaxonExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/ProductsByTaxonExtensionSpec.php index 7a024b77c2..87c362222e 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/ProductsByTaxonExtensionSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/ProductsByTaxonExtensionSpec.php @@ -15,10 +15,12 @@ namespace spec\Sylius\Bundle\ApiBundle\Doctrine\QueryCollectionExtension; use ApiPlatform\Core\Bridge\Doctrine\Orm\Extension\ContextAwareQueryCollectionExtensionInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use Doctrine\ORM\Query\Expr; +use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; use PhpSpec\ObjectBehavior; +use Prophecy\Argument; use Sylius\Bundle\ApiBundle\Context\UserContextInterface; -use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\TaxonInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -79,7 +81,9 @@ final class ProductsByTaxonExtensionSpec extends ObjectBehavior UserInterface $user, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, - ChannelInterface $channel, + Expr $expr, + Expr\Comparison $comparison, + Andx $andx, ): void { $userContext->getUser()->willReturn($user); $user->getRoles()->willReturn([]); @@ -91,7 +95,11 @@ final class ProductsByTaxonExtensionSpec extends ObjectBehavior $queryBuilder->getRootAliases()->willReturn(['o']); $queryBuilder->addSelect('productTaxons')->shouldBeCalled()->willReturn($queryBuilder); $queryBuilder->leftJoin('o.productTaxons', 'productTaxons', 'WITH', 'productTaxons.product = o.id')->shouldBeCalled()->willReturn($queryBuilder); - $queryBuilder->leftJoin('productTaxons.taxon', 'taxon', 'WITH', 'taxon.code IN (:taxonCode)')->shouldBeCalled()->willReturn($queryBuilder); + $expr->andX(Argument::type(Expr\Comparison::class), Argument::type(Expr\Comparison::class))->willReturn($andx); + $expr->in('taxon.code', ':taxonCode')->shouldBeCalled()->willReturn($comparison); + $expr->eq('taxon.enabled', 'true')->shouldBeCalled()->willReturn($comparison); + $queryBuilder->expr()->willReturn($expr->getWrappedObject()); + $queryBuilder->leftJoin('productTaxons.taxon', 'taxon', 'WITH', Argument::type(Andx::class))->shouldBeCalled()->willReturn($queryBuilder); $queryBuilder->orderBy('productTaxons.position', 'ASC')->shouldBeCalled()->willReturn($queryBuilder); $queryBuilder->setParameter('taxonCode', ['t_shirts'])->shouldBeCalled()->willReturn($queryBuilder); diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/TaxonCollectionExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/TaxonCollectionExtensionSpec.php index 35c49fec1d..1d2939dc01 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/TaxonCollectionExtensionSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryCollectionExtension/TaxonCollectionExtensionSpec.php @@ -65,7 +65,7 @@ final class TaxonCollectionExtensionSpec extends ObjectBehavior $queryBuilder->getRootAliases()->shouldBeCalled()->willReturn('o'); $queryBuilder->addSelect('child')->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); $queryBuilder->innerJoin('o.parent', 'parent')->willReturn($queryBuilder->getWrappedObject()); - $queryBuilder->leftJoin('o.children', 'child')->willReturn($queryBuilder->getWrappedObject()); + $queryBuilder->leftJoin('o.children', 'child', 'WITH', 'child.enabled = true')->shouldBeCalled()->willReturn($queryBuilder); $queryBuilder->andWhere('o.enabled = :enabled')->willReturn($queryBuilder->getWrappedObject()); $queryBuilder->andWhere('parent.code = :parentCode')->willReturn($queryBuilder->getWrappedObject()); $queryBuilder->addOrderBy('o.position')->willReturn($queryBuilder->getWrappedObject()); @@ -108,7 +108,7 @@ final class TaxonCollectionExtensionSpec extends ObjectBehavior $queryBuilder->getRootAliases()->shouldBeCalled()->willReturn('o'); $queryBuilder->addSelect('child')->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); $queryBuilder->innerJoin('o.parent', 'parent')->willReturn($queryBuilder->getWrappedObject()); - $queryBuilder->leftJoin('o.children', 'child')->willReturn($queryBuilder->getWrappedObject()); + $queryBuilder->leftJoin('o.children', 'child', 'WITH', 'child.enabled = true')->shouldBeCalled()->willReturn($queryBuilder); $queryBuilder->andWhere('o.enabled = :enabled')->willReturn($queryBuilder->getWrappedObject()); $queryBuilder->andWhere('parent.code = :parentCode')->willReturn($queryBuilder->getWrappedObject()); $queryBuilder->addOrderBy('o.position')->willReturn($queryBuilder->getWrappedObject()); @@ -130,11 +130,7 @@ final class TaxonCollectionExtensionSpec extends ObjectBehavior } function it_does_not_apply_conditions_if_logged_in_user_is_admin( - TaxonRepositoryInterface $taxonRepository, UserContextInterface $userContext, - TaxonInterface $menuTaxon, - TaxonInterface $firstTaxon, - TaxonInterface $secondTaxon, ChannelInterface $channel, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, @@ -142,24 +138,18 @@ final class TaxonCollectionExtensionSpec extends ObjectBehavior ): void { $userContext->getUser()->willReturn($admin); - $channel->getMenuTaxon()->willReturn($menuTaxon); - $admin->getRoles()->shouldBeCalled()->willReturn(['ROLE_API_ACCESS']); - $menuTaxon->getCode()->willReturn('code'); - $queryBuilder->getRootAliases()->shouldNotBeCalled(); $queryBuilder->addSelect('child')->shouldNotBeCalled(); $queryBuilder->innerJoin('o.parent', 'parent')->shouldNotBeCalled(); - $queryBuilder->leftJoin('o.children', 'child')->shouldNotBeCalled(); + $queryBuilder->leftJoin('o.children', 'child', 'WITH', 'child.enabled = true')->shouldNotBeCalled(); $queryBuilder->andWhere('o.enabled = :enabled')->shouldNotBeCalled(); $queryBuilder->andWhere('parent.code = :parentCode')->shouldNotBeCalled(); $queryBuilder->addOrderBy('o.position')->shouldNotBeCalled(); $queryBuilder->setParameter('parentCode', 'code')->shouldNotBeCalled(); $queryBuilder->setParameter('enabled', true)->shouldNotBeCalled(); - $taxonRepository->findChildrenByChannelMenuTaxon($menuTaxon)->willReturn([$firstTaxon, $secondTaxon]); - $this->applyToCollection( $queryBuilder->getWrappedObject(), $queryNameGenerator->getWrappedObject(), diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php new file mode 100644 index 0000000000..0c936fed9b --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryExtension/OrderExtensionSpec.php @@ -0,0 +1,129 @@ +beConstructedWith($sectionProvider, ['cart']); + } + + function it_does_not_apply_conditions_to_collection_for_shop( + QueryBuilder $queryBuilder, + SectionProviderInterface $sectionProvider, + ShopApiSection $shopApiSection, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $sectionProvider->getSection()->willReturn($shopApiSection); + + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + + $this->applyToCollection( + $queryBuilder, + $queryNameGenerator, + OrderInterface::class, + Request::METHOD_GET, + [], + ); + } + + function it_does_not_apply_conditions_to_item_for_shop( + QueryBuilder $queryBuilder, + SectionProviderInterface $sectionProvider, + ShopApiSection $shopApiSection, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $sectionProvider->getSection()->willReturn($shopApiSection); + + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + + $this->applyToItem( + $queryBuilder, + $queryNameGenerator, + OrderInterface::class, + [], + Request::METHOD_GET, + [], + ); + } + + function it_applies_conditions_to_collection_for_admin( + AdminApiSection $adminApiSection, + SectionProviderInterface $sectionProvider, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + Expr $expr, + Expr\Func $exprNotIn, + ): void { + $sectionProvider->getSection()->willReturn($adminApiSection); + + $queryBuilder->getRootAliases()->willReturn(['o']); + + $queryNameGenerator->generateParameterName('state')->shouldBeCalled()->willReturn('state'); + + $queryBuilder->expr()->willReturn($expr); + $expr->notIn('o.state', ':state')->willReturn($exprNotIn); + $queryBuilder->andWhere($exprNotIn)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + $queryBuilder->setParameter('state', ['cart'], ArrayParameterType::STRING)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + + $this->applyToCollection( + $queryBuilder, + $queryNameGenerator, + OrderInterface::class, + Request::METHOD_GET, + ); + } + + function it_applies_conditions_to_item_for_admin( + AdminApiSection $adminApiSection, + SectionProviderInterface $sectionProvider, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + Expr $expr, + Expr\Func $exprNotIn, + ): void { + $queryBuilder->getRootAliases()->willReturn(['o']); + + $sectionProvider->getSection()->willReturn($adminApiSection); + + $queryBuilder->getRootAliases()->willReturn(['o']); + + $queryNameGenerator->generateParameterName('state')->shouldBeCalled()->willReturn('state'); + + $queryBuilder->expr()->willReturn($expr); + $expr->notIn('o.state', ':state')->willReturn($exprNotIn); + $queryBuilder->andWhere($exprNotIn)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + $queryBuilder->setParameter('state', ['cart'], ArrayParameterType::STRING)->shouldBeCalled()->willReturn($queryBuilder->getWrappedObject()); + + $this->applyToItem( + $queryBuilder, + $queryNameGenerator, + OrderInterface::class, + [], + Request::METHOD_GET, + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/EnabledProductInProductAssociationItemExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/EnabledProductInProductAssociationItemExtensionSpec.php new file mode 100644 index 0000000000..3883901f54 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/EnabledProductInProductAssociationItemExtensionSpec.php @@ -0,0 +1,104 @@ +beConstructedWith($userContext); + } + + function it_does_nothing_if_current_resource_is_not_a_product_association( + UserContextInterface $userContext, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $userContext->getUser()->shouldNotBeCalled(); + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + + $this->applyToItem( + $queryBuilder, + $queryNameGenerator, + ProductVariantInterface::class, + [], + Request::METHOD_GET, + ); + } + + function it_does_nothing_if_current_user_is_an_admin_user( + UserContextInterface $userContext, + AdminUserInterface $adminUser, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ): void { + $userContext->getUser()->willReturn($adminUser); + $adminUser->getRoles()->willReturn(['ROLE_API_ACCESS']); + + $queryBuilder->getRootAliases()->shouldNotBeCalled(); + + $this->applyToItem( + $queryBuilder, + $queryNameGenerator, + ProductAssociationInterface::class, + [], + Request::METHOD_GET, + ); + } + + function it_applies_conditions_for_customer( + UserContextInterface $userContext, + UserInterface $user, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ChannelInterface $channel, + ): void { + $userContext->getUser()->willReturn($user); + $user->getRoles()->willReturn([]); + + $queryNameGenerator->generateParameterName('enabled')->shouldBeCalled()->willReturn('enabled'); + $queryNameGenerator->generateParameterName('channel')->shouldBeCalled()->willReturn('channel'); + $queryBuilder->getRootAliases()->willReturn(['o']); + + $queryBuilder->addSelect('associatedProduct')->shouldBeCalled()->willReturn($queryBuilder); + $queryBuilder->leftJoin('o.associatedProducts', 'associatedProduct', 'WITH', 'associatedProduct.enabled = :enabled')->shouldBeCalled()->willReturn($queryBuilder); + $queryBuilder->innerJoin('associatedProduct.channels', 'channel', 'WITH', 'channel = :channel')->shouldBeCalled()->willReturn($queryBuilder); + $queryBuilder->setParameter('enabled', true)->shouldBeCalled()->willReturn($queryBuilder); + $queryBuilder->setParameter('channel', $channel)->shouldBeCalled()->willReturn($queryBuilder); + + $this->applyToItem( + $queryBuilder, + $queryNameGenerator, + ProductAssociationInterface::class, + [], + Request::METHOD_GET, + [ + ContextKeys::CHANNEL => $channel->getWrappedObject(), + ContextKeys::HTTP_REQUEST_METHOD_TYPE => Request::METHOD_GET, + ], + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/OrderShopUserItemExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/OrderShopUserItemExtensionSpec.php index acf7344d3a..2b74ddad16 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/OrderShopUserItemExtensionSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/OrderShopUserItemExtensionSpec.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\ApiBundle\Doctrine\QueryItemExtension; -use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use Doctrine\ORM\QueryBuilder; use PhpSpec\ObjectBehavior; use Prophecy\Argument; @@ -29,7 +29,7 @@ final class OrderShopUserItemExtensionSpec extends ObjectBehavior { function let(UserContextInterface $userContext): void { - $this->beConstructedWith($userContext); + $this->beConstructedWith($userContext, ['shop_select_payment_method', 'shop_account_change_payment_method']); } function it_filters_carts_for_shop_users_to_the_one_owned_by_them_for_methods_other_than_get( diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/OrderVisitorItemExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/OrderVisitorItemExtensionSpec.php index 8c62ce1481..5bff01c069 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/OrderVisitorItemExtensionSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/OrderVisitorItemExtensionSpec.php @@ -28,7 +28,7 @@ final class OrderVisitorItemExtensionSpec extends ObjectBehavior { function let(UserContextInterface $userContext): void { - $this->beConstructedWith($userContext); + $this->beConstructedWith($userContext, ['shop_select_payment_method']); } function it_filters_carts_for_visitors_to_not_authorized_for_methods_other_than_get( diff --git a/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/TaxonItemExtensionSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/TaxonItemExtensionSpec.php new file mode 100644 index 0000000000..58a9cf5633 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Doctrine/QueryItemExtension/TaxonItemExtensionSpec.php @@ -0,0 +1,67 @@ +beConstructedWith($userContext); + } + + function it_does_not_apply_extension_if_resource_class_is_not_taxon_interface( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + ) { + $this->applyToItem($queryBuilder, $queryNameGenerator, ProductVariantInterface::class, [], null, []); + } + + function it_does_not_apply_extension_if_user_has_api_access_role( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + UserContextInterface $userContext, + UserInterface $user, + ) { + $userContext->getUser()->willReturn($user); + $user->getRoles()->willReturn(['ROLE_API_ACCESS']); + + $this->applyToItem($queryBuilder, $queryNameGenerator, TaxonInterface::class, [], null, []); + } + + function it_applies_extension_to_item_query( + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + UserContextInterface $userContext, + ) { + $userContext->getUser()->willReturn(null); + $queryBuilder->getRootAliases()->willReturn(['rootAlias']); + $queryNameGenerator->generateParameterName('enabled')->willReturn('enabled'); + $queryNameGenerator->generateJoinAlias('child')->willReturn('childAlias'); + + $queryBuilder->addSelect('childAlias')->shouldBeCalled(); + $queryBuilder->leftJoin('rootAlias.children', 'childAlias', 'WITH', 'childAlias.enabled = :enabled')->shouldBeCalled()->willReturn($queryBuilder); + $queryBuilder->andWhere('rootAlias.enabled = :enabled')->shouldBeCalled()->willReturn($queryBuilder); + $queryBuilder->setParameter('enabled', true)->shouldBeCalled()->willReturn($queryBuilder); + + $this->applyToItem($queryBuilder, $queryNameGenerator, TaxonInterface::class, [], null, []); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/EventListener/AdminAuthenticationSuccessListenerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/EventListener/AdminAuthenticationSuccessListenerSpec.php new file mode 100644 index 0000000000..4a5e771fd2 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/EventListener/AdminAuthenticationSuccessListenerSpec.php @@ -0,0 +1,51 @@ +beConstructedWith($iriConverter); + } + + function it_adds_admins_to_admin_authentication_token_response( + IriConverterInterface $iriConverter, + AdminUserInterface $adminUser, + ): void { + $event = new AuthenticationSuccessEvent([], $adminUser->getWrappedObject(), new Response()); + $iriConverter->getIriFromResource($adminUser->getWrappedObject())->shouldBeCalled(); + + $this->onAuthenticationSuccessResponse($event); + } + + function it_does_not_add_anything_to_shop_authentication_token_response( + IriConverterInterface $iriConverter, + AdminUserInterface $adminUser, + ShopUserInterface $shopUser, + ): void { + $event = new AuthenticationSuccessEvent([], $shopUser->getWrappedObject(), new Response()); + + $iriConverter->getIriFromResource($adminUser->getWrappedObject())->shouldNotBeCalled(); + + $this->onAuthenticationSuccessResponse($event); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/EventListener/AuthenticationSuccessListenerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/EventListener/AuthenticationSuccessListenerSpec.php index 39084a479b..25efcee07f 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/EventListener/AuthenticationSuccessListenerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/EventListener/AuthenticationSuccessListenerSpec.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\ApiBundle\EventListener; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent; use PhpSpec\ObjectBehavior; use Sylius\Component\Core\Model\AdminUserInterface; @@ -37,7 +37,7 @@ final class AuthenticationSuccessListenerSpec extends ObjectBehavior $shopUser->getCustomer()->willReturn($customer->getWrappedObject()); - $iriConverter->getIriFromItem($customer->getWrappedObject())->shouldBeCalled(); + $iriConverter->getIriFromResource($customer->getWrappedObject())->shouldBeCalled(); $this->onAuthenticationSuccessResponse($event); } @@ -49,7 +49,7 @@ final class AuthenticationSuccessListenerSpec extends ObjectBehavior ): void { $event = new AuthenticationSuccessEvent([], $adminUser->getWrappedObject(), new Response()); - $iriConverter->getIriFromItem($customer->getWrappedObject())->shouldNotBeCalled(); + $iriConverter->getIriFromResource($customer->getWrappedObject())->shouldNotBeCalled(); $this->onAuthenticationSuccessResponse($event); } diff --git a/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/AttributeEventSubscriberSpec.php b/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/AttributeEventSubscriberSpec.php new file mode 100644 index 0000000000..efd21254b0 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/AttributeEventSubscriberSpec.php @@ -0,0 +1,138 @@ +beConstructedWith($registry); + } + + function it_implements_event_subscriber_interface(): void + { + $this->shouldImplement(EventSubscriberInterface::class); + } + + function it_does_nothing_when_controller_result_is_not_an_attribute( + ServiceRegistryInterface $registry, + HttpKernelInterface $kernel, + Request $request, + ): void { + $request->getMethod()->shouldBeCalled(); + $registry->has(Argument::any())->shouldNotBeCalled(); + + $this->assignStorageType(new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MAIN_REQUEST, + new \stdClass(), + )); + } + + function it_does_nothing_when_attribute_has_no_type( + ServiceRegistryInterface $registry, + HttpKernelInterface $kernel, + Request $request, + AttributeInterface $attribute, + ): void { + $request->getMethod()->willReturn(Request::METHOD_POST); + $attribute->getType()->willReturn(null); + + $registry->has(Argument::any())->shouldNotBeCalled(); + + $this->assignStorageType(new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MAIN_REQUEST, + $attribute->getWrappedObject(), + )); + } + + function it_does_nothing_when_attribute_has_a_storage_type( + ServiceRegistryInterface $registry, + HttpKernelInterface $kernel, + Request $request, + AttributeInterface $attribute, + ): void { + $request->getMethod()->willReturn(Request::METHOD_POST); + $attribute->getType()->willReturn('text'); + $attribute->getStorageType()->willReturn('text'); + + $registry->has(Argument::any())->shouldNotBeCalled(); + + $this->assignStorageType(new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MAIN_REQUEST, + $attribute->getWrappedObject(), + )); + } + + function it_does_nothing_when_attribute_type_is_not_registered( + ServiceRegistryInterface $registry, + HttpKernelInterface $kernel, + Request $request, + AttributeInterface $attribute, + ): void { + $request->getMethod()->willReturn(Request::METHOD_POST); + $attribute->getType()->willReturn('foo'); + $attribute->getStorageType()->willReturn(null); + + $registry->has('foo')->willReturn(false); + + $this->assignStorageType(new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MAIN_REQUEST, + $attribute->getWrappedObject(), + )); + } + + function it_sets_storage_type_based_on_set_attribute_type( + ServiceRegistryInterface $registry, + HttpKernelInterface $kernel, + Request $request, + AttributeInterface $attribute, + AttributeTypeInterface $attributeType, + ): void { + $request->getMethod()->willReturn(Request::METHOD_POST); + $attribute->getType()->willReturn('foo'); + $attribute->getStorageType()->willReturn(null); + + $registry->has('foo')->willReturn(true); + $registry->get('foo')->willReturn($attributeType); + + $attributeType->getStorageType()->willReturn('bar'); + + $attribute->setStorageType('bar')->shouldBeCalled(); + + $this->assignStorageType(new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MAIN_REQUEST, + $attribute->getWrappedObject(), + )); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/ProductDeletionEventSubscriberSpec.php b/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/ProductDeletionEventSubscriberSpec.php index 6df20358c8..3a7a4792f9 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/ProductDeletionEventSubscriberSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/ProductDeletionEventSubscriberSpec.php @@ -43,12 +43,32 @@ final class ProductDeletionEventSubscriberSpec extends ObjectBehavior $product->getWrappedObject(), ); + $productInPromotionRuleChecker->isInUse($product)->shouldNotBeCalled(); + + $this->shouldNotThrow()->during('protectFromRemovingProductInUseByPromotionRule', [$event]); + } + + function it_does_not_throw_exception_when_product_is_not_in_use_by_a_promotion_rule( + ProductInPromotionRuleCheckerInterface $productInPromotionRuleChecker, + ProductInterface $product, + Request $request, + HttpKernelInterface $kernel, + ): void { + $request->getMethod()->willReturn(Request::METHOD_POST); + + $event = new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MAIN_REQUEST, + $product->getWrappedObject(), + ); + $productInPromotionRuleChecker->isInUse($product)->willReturn(false); $this->shouldNotThrow()->during('protectFromRemovingProductInUseByPromotionRule', [$event]); } - function it_throws_an_exception_when_trying_to_delete_product_assigned_to_promotion_rule( + function it_throws_an_exception_when_trying_to_delete_product_that_is_in_use_by_a_promotion_rule( ProductInPromotionRuleCheckerInterface $productInPromotionRuleChecker, ProductInterface $product, Request $request, diff --git a/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/TaxonDeletionEventSubscriberSpec.php b/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/TaxonDeletionEventSubscriberSpec.php new file mode 100644 index 0000000000..4f0b4649e3 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/TaxonDeletionEventSubscriberSpec.php @@ -0,0 +1,151 @@ +beConstructedWith($channelRepository, $taxonInPromotionRuleChecker); + } + + function it_allows_to_remove_taxon_if_any_channel_has_not_it_as_a_menu_taxon( + TaxonInterface $taxon, + HttpKernelInterface $kernel, + Request $request, + ChannelRepositoryInterface $channelRepository, + ): void { + $request->getMethod()->willReturn(Request::METHOD_DELETE); + $taxon->getCode()->willReturn('WATCHES'); + $channelRepository->findOneBy(['menuTaxon' => $taxon])->willReturn(null); + + $this->protectFromRemovingMenuTaxon(new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MASTER_REQUEST, + $taxon->getWrappedObject(), + )); + } + + function it_does_nothing_after_writing_other_entity( + HttpKernelInterface $kernel, + Request $request, + ): void { + $request->getMethod()->willReturn(Request::METHOD_DELETE); + + $this->protectFromRemovingMenuTaxon(new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MASTER_REQUEST, + new \stdClass(), + )); + } + + function it_throws_an_exception_if_a_subject_is_menu_taxon( + TaxonInterface $taxon, + HttpKernelInterface $kernel, + Request $request, + ChannelRepositoryInterface $channelRepository, + ChannelInterface $channel, + ): void { + $request->getMethod()->willReturn(Request::METHOD_DELETE); + $taxon->getCode()->willReturn('WATCHES'); + $channelRepository->findOneBy(['menuTaxon' => $taxon])->willReturn($channel); + + $this + ->shouldThrow(\Exception::class) + ->during('protectFromRemovingMenuTaxon', [new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MASTER_REQUEST, + $taxon->getWrappedObject(), + )]) + ; + } + + function it_does_not_throw_exception_when_taxon_is_not_being_deleted( + TaxonInPromotionRuleCheckerInterface $taxonInPromotionRuleChecker, + TaxonInterface $taxon, + Request $request, + HttpKernelInterface $kernel, + ): void { + $request->getMethod()->willReturn(Request::METHOD_POST); + + $event = new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MAIN_REQUEST, + $taxon->getWrappedObject(), + ); + + $taxonInPromotionRuleChecker->isInUse($taxon)->shouldNotBeCalled(); + + $this->shouldNotThrow()->during('protectFromRemovingTaxonInUseByPromotionRule', [$event]); + } + + function it_does_not_throw_exception_when_taxon_is_not_in_use_by_a_promotion_rule( + TaxonInPromotionRuleCheckerInterface $taxonInPromotionRuleChecker, + TaxonInterface $taxon, + Request $request, + HttpKernelInterface $kernel, + ): void { + $request->getMethod()->willReturn(Request::METHOD_POST); + + $event = new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MAIN_REQUEST, + $taxon->getWrappedObject(), + ); + + $taxonInPromotionRuleChecker->isInUse($taxon)->willReturn(false); + + $this->shouldNotThrow()->during('protectFromRemovingTaxonInUseByPromotionRule', [$event]); + } + + function it_throws_an_exception_when_trying_to_delete_taxon_that_is_in_use_by_a_promotion_rule( + TaxonInPromotionRuleCheckerInterface $taxonInPromotionRuleChecker, + TaxonInterface $taxon, + Request $request, + HttpKernelInterface $kernel, + ): void { + $request->getMethod()->willReturn(Request::METHOD_DELETE); + + $event = new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MAIN_REQUEST, + $taxon->getWrappedObject(), + ); + + $taxonInPromotionRuleChecker->isInUse($taxon)->willReturn(true); + + $this + ->shouldThrow(TaxonCannotBeRemoved::class) + ->during('protectFromRemovingTaxonInUseByPromotionRule', [$event]) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/TaxonSlugEventSubscriberSpec.php b/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/TaxonSlugEventSubscriberSpec.php new file mode 100644 index 0000000000..1c726f7e1b --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/EventSubscriber/TaxonSlugEventSubscriberSpec.php @@ -0,0 +1,106 @@ +beConstructedWith($taxonSlugGenerator); + } + + function it_generates_slug_for_taxon_with_name_and_empty_slug( + TaxonSlugGeneratorInterface $taxonSlugGenerator, + TaxonInterface $taxon, + TaxonTranslationInterface $taxonTranslation, + HttpKernelInterface $kernel, + Request $request, + ): void { + $request->getMethod()->willReturn(Request::METHOD_POST); + + $taxon->getTranslations()->willReturn(new ArrayCollection([$taxonTranslation->getWrappedObject()])); + $taxonTranslation->getSlug()->willReturn(null); + $taxonTranslation->getName()->willReturn('PHP Mug'); + $taxonTranslation->getLocale()->willReturn('en_US'); + + $taxonSlugGenerator->generate($taxon, 'en_US')->willReturn('php-mug'); + + $taxonTranslation->setSlug('php-mug')->shouldBeCalled(); + + $this->generateSlug(new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MASTER_REQUEST, + $taxon->getWrappedObject(), + )); + } + + function it_does_nothing_if_the_taxon_has_slug( + TaxonSlugGeneratorInterface $taxonSlugGenerator, + TaxonInterface $taxon, + TaxonTranslationInterface $taxonTranslation, + HttpKernelInterface $kernel, + Request $request, + ): void { + $request->getMethod()->willReturn(Request::METHOD_POST); + + $taxon->getTranslations()->willReturn(new ArrayCollection([$taxonTranslation->getWrappedObject()])); + $taxonTranslation->getSlug()->willReturn('php-mug'); + + $taxonTranslation->getName()->shouldNotBeCalled(); + $taxonSlugGenerator->generate(Argument::any())->shouldNotBeCalled(); + $taxonTranslation->setSlug(Argument::any())->shouldNotBeCalled(); + + $this->generateSlug(new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MASTER_REQUEST, + $taxon->getWrappedObject(), + )); + } + + function it_does_nothing_if_the_taxon_has_no_name( + TaxonSlugGeneratorInterface $taxonSlugGenerator, + TaxonInterface $taxon, + TaxonTranslationInterface $taxonTranslation, + HttpKernelInterface $kernel, + Request $request, + ): void { + $request->getMethod()->willReturn(Request::METHOD_POST); + + $taxon->getTranslations()->willReturn(new ArrayCollection([$taxonTranslation->getWrappedObject()])); + $taxonTranslation->getSlug()->willReturn(null); + $taxonTranslation->getName()->willReturn(null); + + $taxonSlugGenerator->generate(Argument::any())->shouldNotBeCalled(); + $taxonTranslation->setSlug(Argument::any())->shouldNotBeCalled(); + + $this->generateSlug(new ViewEvent( + $kernel->getWrappedObject(), + $request->getWrappedObject(), + HttpKernelInterface::MASTER_REQUEST, + $taxon->getWrappedObject(), + )); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Filter/Doctrine/TaxonFilterSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Filter/Doctrine/TaxonFilterSpec.php index 8aafbfec49..fe47eb21ed 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Filter/Doctrine/TaxonFilterSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Filter/Doctrine/TaxonFilterSpec.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\ApiBundle\Filter\Doctrine; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\Exception\ItemNotFoundException; @@ -36,7 +36,7 @@ final class TaxonFilterSpec extends ObjectBehavior QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, ): void { - $iriConverter->getItemFromIri('api/taxon')->willReturn($taxon); + $iriConverter->getResourceFromIri('api/taxon')->willReturn($taxon); $queryBuilder->getRootAliases()->willReturn(['o']); $queryBuilder->distinct()->willReturn($queryBuilder); @@ -72,7 +72,7 @@ final class TaxonFilterSpec extends ObjectBehavior QueryNameGeneratorInterface $queryNameGenerator, ): void { $context['filters']['order'] = ['differentOrderParameter' => 'asc']; - $iriConverter->getItemFromIri('api/taxon')->willReturn($taxon); + $iriConverter->getResourceFromIri('api/taxon')->willReturn($taxon); $queryBuilder->getRootAliases()->willReturn(['o']); $queryBuilder->distinct()->willReturn($queryBuilder); @@ -103,7 +103,7 @@ final class TaxonFilterSpec extends ObjectBehavior QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, ): void { - $iriConverter->getItemFromIri('api/taxon')->willThrow(ItemNotFoundException::class); + $iriConverter->getResourceFromIri('api/taxon')->willThrow(ItemNotFoundException::class); $queryBuilder->getRootAliases()->willReturn(['o']); $queryBuilder->distinct()->willReturn($queryBuilder); @@ -131,7 +131,7 @@ final class TaxonFilterSpec extends ObjectBehavior QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, ): void { - $iriConverter->getItemFromIri('non-existing-taxon')->willThrow(InvalidArgumentException::class); + $iriConverter->getResourceFromIri('non-existing-taxon')->willThrow(InvalidArgumentException::class); $queryBuilder->getRootAliases()->willReturn(['o']); $queryBuilder->distinct()->willReturn($queryBuilder); diff --git a/src/Sylius/Bundle/ApiBundle/spec/Modifier/OrderAddressModifierSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Modifier/OrderAddressModifierSpec.php index b4dfae7b42..58b7a1b2d3 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Modifier/OrderAddressModifierSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Modifier/OrderAddressModifierSpec.php @@ -16,7 +16,8 @@ namespace spec\Sylius\Bundle\ApiBundle\Modifier; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; -use SM\StateMachine\StateMachineInterface; +use SM\StateMachine\StateMachineInterface as WinzouStateMachineInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\ApiBundle\Mapper\AddressMapperInterface; use Sylius\Component\Core\Model\AddressInterface; use Sylius\Component\Core\Model\ChannelInterface; @@ -37,7 +38,7 @@ final class OrderAddressModifierSpec extends ObjectBehavior AddressInterface $billingAddress, OrderInterface $order, ChannelInterface $channel, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $order->getTokenValue()->willReturn('ORDERTOKEN'); $order->getShippingAddress()->willReturn(null); @@ -61,7 +62,7 @@ final class OrderAddressModifierSpec extends ObjectBehavior AddressInterface $shippingAddress, OrderInterface $order, ChannelInterface $channel, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $order->getTokenValue()->willReturn('ORDERTOKEN'); $order->getShippingAddress()->willReturn(null); @@ -86,7 +87,7 @@ final class OrderAddressModifierSpec extends ObjectBehavior AddressInterface $shippingAddress, OrderInterface $order, ChannelInterface $channel, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $order->getTokenValue()->willReturn('ORDERTOKEN'); $order->getShippingAddress()->willReturn(null); @@ -105,6 +106,32 @@ final class OrderAddressModifierSpec extends ObjectBehavior $this->modify($order, $billingAddress->getWrappedObject(), $shippingAddress->getWrappedObject(), 'r2d2@droid.com'); } + function it_uses_the_new_state_machine_abstraction_if_provided( + StateMachineInterface $stateMachine, + AddressMapperInterface $addressMapper, + AddressInterface $billingAddress, + AddressInterface $shippingAddress, + OrderInterface $order, + ChannelInterface $channel, + ): void { + $this->beConstructedWith($stateMachine, $addressMapper); + + $order->getTokenValue()->willReturn('ORDERTOKEN'); + $order->getShippingAddress()->willReturn(null); + $order->getBillingAddress()->willReturn(null); + $order->getChannel()->willReturn($channel); + + $channel->isShippingAddressInCheckoutRequired()->willReturn(false); + + $order->setBillingAddress($billingAddress)->shouldBeCalled(); + $order->setShippingAddress($shippingAddress)->shouldBeCalled(); + + $stateMachine->can($order, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_ADDRESS)->willReturn(true); + $stateMachine->apply($order, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_ADDRESS)->shouldBeCalled(); + + $this->modify($order, $billingAddress->getWrappedObject(), $shippingAddress->getWrappedObject(), 'r2d2@droid.com'); + } + function it_updates_order_addresses( StateMachineFactoryInterface $stateMachineFactory, AddressMapperInterface $addressMapper, @@ -114,7 +141,7 @@ final class OrderAddressModifierSpec extends ObjectBehavior AddressInterface $oldShippingAddress, OrderInterface $order, ChannelInterface $channel, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $order->getTokenValue()->willReturn('ORDERTOKEN'); $order->getBillingAddress()->willReturn($oldBillingAddress); @@ -141,7 +168,7 @@ final class OrderAddressModifierSpec extends ObjectBehavior AddressInterface $billingAddress, AddressInterface $shippingAddress, OrderInterface $order, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH)->willReturn($stateMachine); $stateMachine->can(OrderCheckoutTransitions::TRANSITION_ADDRESS)->willReturn(false); diff --git a/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryCollectionHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryCollectionHandlerSpec.php new file mode 100644 index 0000000000..1edc59a3ee --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetAddressLogEntryCollectionHandlerSpec.php @@ -0,0 +1,43 @@ +beConstructedWith($addressLogEntryRepository); + } + + function it_returns_address_log_entries_for_a_given_customer( + ResourceLogEntryRepositoryInterface $addressLogEntryRepository, + QueryBuilder $queryBuilder, + AbstractQuery $query, + LogEntryInterface $addressLogEntryOne, + LogEntryInterface $addressLogEntryTwo, + ): void { + $query->getResult()->willReturn([$addressLogEntryOne, $addressLogEntryTwo]); + $queryBuilder->getQuery()->willReturn($query); + $addressLogEntryRepository->createByObjectIdQueryBuilder('3')->willReturn($queryBuilder); + + $this(new GetAddressLogEntryCollection(3))->shouldIterateAs([$addressLogEntryOne, $addressLogEntryTwo]); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetCustomerStatisticsHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetCustomerStatisticsHandlerSpec.php new file mode 100644 index 0000000000..f434a49f7e --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetCustomerStatisticsHandlerSpec.php @@ -0,0 +1,56 @@ +beConstructedWith($customerRepository, $customerStatisticsProvider); + } + + function it_returns_statistics_for_a_given_customer( + CustomerRepositoryInterface $customerRepository, + CustomerStatisticsProviderInterface $customerStatisticsProvider, + CustomerInterface $customer, + ): void { + $customerStatistics = new CustomerStatistics([]); + + $customerRepository->find(1)->willReturn($customer); + $customerStatisticsProvider->getCustomerStatistics($customer)->willReturn($customerStatistics); + + $query = new GetCustomerStatistics(1); + $this($query)->shouldReturn($customerStatistics); + } + + function it_throws_an_exception_when_customer_with_a_given_id_doesnt_exist(CustomerRepositoryInterface $customerRepository): void + { + $customerRepository->find(1)->willReturn(null); + + $query = new GetCustomerStatistics(1); + + $this + ->shouldThrow(CustomerNotFoundException::class) + ->during('__invoke', [$query]) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetStatisticsHandlerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetStatisticsHandlerSpec.php new file mode 100644 index 0000000000..8fb32a9abb --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/QueryHandler/GetStatisticsHandlerSpec.php @@ -0,0 +1,65 @@ +beConstructedWith($statisticsProvider, $channelRepository); + } + + function it_gets_statistics( + StatisticsProviderInterface $statisticsProvider, + ChannelRepositoryInterface $channelRepository, + ChannelInterface $channel, + GetStatistics $query, + \DatePeriod $datePeriod, + Statistics $statistics, + ): void { + $query->getChannelCode()->willReturn('CHANNEL_CODE'); + $query->getDatePeriod()->willReturn($datePeriod); + $query->getIntervalType()->willReturn('day'); + + $channelRepository->findOneByCode('CHANNEL_CODE')->willReturn($channel); + + $statisticsProvider->provide('day', $datePeriod, $channel)->willReturn($statistics); + + $this->__invoke($query)->shouldBe($statistics); + } + + function it_throws_channel_not_found_exception_when_channel_is_null( + ChannelRepositoryInterface $channelRepository, + ): void { + $datePeriod = new \DatePeriod( + new \DateTime('2022-01-01'), + new \DateInterval('P1D'), + new \DateTime('2022-12-31'), + ); + $channelRepository->findOneByCode('NON_EXISTING_CHANNEL_CODE')->willReturn(null); + + $this + ->shouldThrow(ChannelNotFoundException::class) + ->during('__invoke', [new GetStatistics('day', $datePeriod, 'NON_EXISTING_CHANNEL_CODE')]) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ChannelDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ChannelDenormalizerSpec.php new file mode 100644 index 0000000000..64770e448e --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ChannelDenormalizerSpec.php @@ -0,0 +1,117 @@ +beConstructedWith($configFactory, $shopBillingDataFactory); + } + + function it_does_not_support_denormalization_when_the_denormalizer_has_already_been_called(): void + { + $this->supportsDenormalization([], ChannelInterface::class, context: [self::ALREADY_CALLED => true])->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_data_is_not_an_array(): void + { + $this->supportsDenormalization('string', ChannelInterface::class)->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_type_is_not_a_channel(): void + { + $this->supportsDenormalization([], 'string')->shouldReturn(false); + } + + function it_throws_an_exception_when_denormalizing_an_object_that_is_not_a_channel( + DenormalizerInterface $denormalizer, + ): void { + $this->setDenormalizer($denormalizer); + + $denormalizer->denormalize([], 'string', null, [self::ALREADY_CALLED => true])->willReturn(new \stdClass()); + + $this->shouldThrow(\InvalidArgumentException::class)->during('denormalize', [[], 'string']); + } + + function it_returns_channel_as_is_when_shop_billing_data_and_channel_price_history_config_has_already_been_set( + DenormalizerInterface $denormalizer, + FactoryInterface $configFactory, + ShopBillingDataInterface $shopBillingData, + ChannelPriceHistoryConfigInterface $config, + ChannelInterface $channel, + ): void { + $this->setDenormalizer($denormalizer); + + $channel->getChannelPriceHistoryConfig()->willReturn($config); + $channel->getShopBillingData()->willReturn($shopBillingData); + + $channel->setChannelPriceHistoryConfig(Argument::any())->shouldNotBeCalled(); + $configFactory->createNew()->shouldNotBeCalled(); + + $denormalizer->denormalize([], ChannelInterface::class, null, [self::ALREADY_CALLED => true])->willReturn($channel); + + $this->denormalize([], ChannelInterface::class)->shouldReturn($channel); + } + + function it_adds_a_new_channel_price_history_config_when_channel_has_none( + DenormalizerInterface $denormalizer, + FactoryInterface $configFactory, + ShopBillingDataInterface $shopBillingData, + ChannelPriceHistoryConfigInterface $config, + ChannelInterface $channel, + ): void { + $this->setDenormalizer($denormalizer); + + $channel->getChannelPriceHistoryConfig()->willReturn(null); + $channel->getShopBillingData()->willReturn($shopBillingData); + + $configFactory->createNew()->willReturn($config); + $channel->setChannelPriceHistoryConfig($config)->shouldBeCalled(); + + $denormalizer->denormalize([], ChannelInterface::class, null, [self::ALREADY_CALLED => true])->willReturn($channel); + + $this->denormalize([], ChannelInterface::class)->shouldReturn($channel); + } + + function it_adds_a_new_shop_billing_data_when_channel_has_none( + DenormalizerInterface $denormalizer, + FactoryInterface $shopBillingDataFactory, + ShopBillingDataInterface $shopBillingData, + ChannelPriceHistoryConfigInterface $config, + ChannelInterface $channel, + ): void { + $this->setDenormalizer($denormalizer); + + $channel->getChannelPriceHistoryConfig()->willReturn($config); + $channel->getShopBillingData()->willReturn(null); + + $shopBillingDataFactory->createNew()->willReturn($shopBillingData); + $channel->setShopBillingData($shopBillingData)->shouldBeCalled(); + + $denormalizer->denormalize([], ChannelInterface::class, null, [self::ALREADY_CALLED => true])->willReturn($channel); + + $this->denormalize([], ChannelInterface::class)->shouldReturn($channel); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ChannelPriceHistoryConfigDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ChannelPriceHistoryConfigDenormalizerSpec.php new file mode 100644 index 0000000000..c2344a9e93 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ChannelPriceHistoryConfigDenormalizerSpec.php @@ -0,0 +1,197 @@ +beConstructedWith($iriConverter, $configFactory, $validator, []); + } + + function it_does_not_support_denormalization_when_the_denormalizer_has_already_been_called(): void + { + $this->supportsDenormalization([], 'string', context: [self::ALREADY_CALLED => true])->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_data_is_not_an_array(): void + { + $this->supportsDenormalization('string', 'string')->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_type_is_not_a_channel_price_history_config(): void + { + $this->supportsDenormalization([], 'string')->shouldReturn(false); + } + + function it_throws_an_exception_when_denormalizing_an_object_that_is_not_a_channel_price_history_config( + DenormalizerInterface $denormalizer, + FactoryInterface $configFactory, + ResourceInputDataPropertiesValidatorInterface $validator, + ChannelPriceHistoryConfigInterface $dummyConfig, + ): void { + $this->setDenormalizer($denormalizer); + + $configFactory->createNew()->willReturn($dummyConfig); + $validator->validate($dummyConfig, [], [])->shouldBeCalled(); + + $denormalizer->denormalize([], 'string', null, [self::ALREADY_CALLED => true])->willReturn(new \stdClass()); + + $this->shouldThrow(\InvalidArgumentException::class)->during('denormalize', [[], 'string']); + } + + function it_validates_input_data_before( + DenormalizerInterface $denormalizer, + FactoryInterface $configFactory, + ResourceInputDataPropertiesValidatorInterface $validator, + ChannelPriceHistoryConfigInterface $dummyConfig, + ): void { + $this->setDenormalizer($denormalizer); + + $configFactory->createNew()->willReturn($dummyConfig); + $validator->validate($dummyConfig, [], [])->willThrow(ValidationException::class); + + $denormalizer->denormalize(Argument::cetera())->shouldNotBeCalled(); + + $this->shouldThrow(ValidationException::class)->during('denormalize', [[], 'string']); + } + + function it_adds_excluded_taxons_from_data( + DenormalizerInterface $denormalizer, + IriConverterInterface $iriConverter, + FactoryInterface $configFactory, + ResourceInputDataPropertiesValidatorInterface $validator, + TaxonInterface $firstTaxon, + TaxonInterface $secondTaxon, + ChannelPriceHistoryConfigInterface $config, + ChannelPriceHistoryConfigInterface $dummyConfig, + ): void { + $this->setDenormalizer($denormalizer); + + $data = ['taxonsExcludedFromShowingLowestPrice' => [ + '/api/v2/taxons/first-new-taxon', + '/api/v2/taxons/second-new-taxon', + ]]; + + $configFactory->createNew()->willReturn($dummyConfig); + $validator->validate($dummyConfig, $data, [])->shouldBeCalled(); + + $denormalizer->denormalize($data, 'string', null, [self::ALREADY_CALLED => true])->willReturn($config); + + $config->clearTaxonsExcludedFromShowingLowestPrice()->shouldBeCalled(); + + $iriConverter->getResourceFromIri('/api/v2/taxons/first-new-taxon')->shouldBeCalledTimes(1)->willReturn($firstTaxon); + $iriConverter->getResourceFromIri('/api/v2/taxons/second-new-taxon')->shouldBeCalledTimes(1)->willReturn($secondTaxon); + + $config->addTaxonExcludedFromShowingLowestPrice($firstTaxon)->shouldBeCalledTimes(1); + $config->addTaxonExcludedFromShowingLowestPrice($secondTaxon)->shouldBeCalledTimes(1); + + $this->denormalize($data, 'string')->shouldReturn($config); + } + + function it_removes_excluded_taxons_when_data_has_none( + DenormalizerInterface $denormalizer, + IriConverterInterface $iriConverter, + FactoryInterface $configFactory, + ResourceInputDataPropertiesValidatorInterface $validator, + TaxonInterface $firstTaxon, + TaxonInterface $secondTaxon, + ChannelPriceHistoryConfigInterface $config, + ChannelPriceHistoryConfigInterface $dummyConfig, + ): void { + $this->setDenormalizer($denormalizer); + + $data = []; + + $configFactory->createNew()->willReturn($dummyConfig); + $validator->validate($dummyConfig, $data, [])->shouldBeCalled(); + + $denormalizer + ->denormalize($data, 'string', null, [self::ALREADY_CALLED => true]) + ->willReturn($config) + ; + + $config->getTaxonsExcludedFromShowingLowestPrice()->willReturn(new ArrayCollection([ + $firstTaxon->getWrappedObject(), + $secondTaxon->getWrappedObject(), + ])); + + $config->clearTaxonsExcludedFromShowingLowestPrice()->shouldBeCalled(); + + $iriConverter->getResourceFromIri(Argument::cetera())->shouldNotBeCalled(); + + $config->addTaxonExcludedFromShowingLowestPrice(Argument::any())->shouldNotBeCalled(); + + $this->denormalize($data, 'string')->shouldReturn($config); + } + + function it_replaces_current_excluded_taxons_with_ones_from_data( + DenormalizerInterface $denormalizer, + IriConverterInterface $iriConverter, + FactoryInterface $configFactory, + ResourceInputDataPropertiesValidatorInterface $validator, + TaxonInterface $firstCurrentTaxon, + TaxonInterface $secondCurrentTaxon, + TaxonInterface $firstNewTaxon, + TaxonInterface $secondNewTaxon, + ChannelPriceHistoryConfigInterface $config, + ChannelPriceHistoryConfigInterface $dummyConfig, + ): void { + $this->setDenormalizer($denormalizer); + + $data = ['taxonsExcludedFromShowingLowestPrice' => [ + '/api/v2/taxons/first-new-taxon', + '/api/v2/taxons/second-new-taxon', + ]]; + + $configFactory->createNew()->willReturn($dummyConfig); + $validator->validate($dummyConfig, $data, [])->shouldBeCalled(); + + $denormalizer + ->denormalize($data, 'string', null, [self::ALREADY_CALLED => true]) + ->willReturn($config) + ; + + $config->getTaxonsExcludedFromShowingLowestPrice()->willReturn(new ArrayCollection([ + $firstCurrentTaxon->getWrappedObject(), + $secondCurrentTaxon->getWrappedObject(), + ])); + + $config->clearTaxonsExcludedFromShowingLowestPrice()->shouldBeCalled(); + + $iriConverter->getResourceFromIri('/api/v2/taxons/first-new-taxon')->shouldBeCalledTimes(1)->willReturn($firstNewTaxon); + $iriConverter->getResourceFromIri('/api/v2/taxons/second-new-taxon')->shouldBeCalledTimes(1)->willReturn($secondNewTaxon); + + $config->addTaxonExcludedFromShowingLowestPrice($firstNewTaxon)->shouldBeCalledTimes(1); + $config->addTaxonExcludedFromShowingLowestPrice($secondNewTaxon)->shouldBeCalledTimes(1); + + $this->denormalize($data, 'string')->shouldReturn($config); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandArgumentsDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandArgumentsDenormalizerSpec.php index 2df5e2da94..fde5b123d1 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandArgumentsDenormalizerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandArgumentsDenormalizerSpec.php @@ -16,6 +16,7 @@ namespace spec\Sylius\Bundle\ApiBundle\Serializer; use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use PhpSpec\ObjectBehavior; use Sylius\Bundle\ApiBundle\Command\Catalog\AddProductReview; +use Sylius\Bundle\ApiBundle\Command\IriToIdentifierConversionAwareInterface; use Sylius\Bundle\ApiBundle\Converter\IriToIdentifierConverterInterface; use Sylius\Component\Core\Model\Order; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -23,12 +24,12 @@ use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; final class CommandArgumentsDenormalizerSpec extends ObjectBehavior { function let( - DenormalizerInterface $objectNormalizer, + DenormalizerInterface $commandDenormalizer, IriToIdentifierConverterInterface $iriToIdentifierConverter, DataTransformerInterface $commandAwareInputDataTransformer, ): void { $this->beConstructedWith( - $objectNormalizer, + $commandDenormalizer, $iriToIdentifierConverter, $commandAwareInputDataTransformer, ); @@ -36,7 +37,7 @@ final class CommandArgumentsDenormalizerSpec extends ObjectBehavior function it_supports_denormalization_add_product_review(): void { - $context['input']['class'] = AddProductReview::class; + $context = ['input' => ['class' => AddProductReview::class]]; $this ->supportsDenormalization( @@ -51,7 +52,7 @@ final class CommandArgumentsDenormalizerSpec extends ObjectBehavior function it_does_not_support_denormalization_for_not_supported_class(): void { - $context['input']['class'] = Order::class; + $context = ['input' => ['class' => Order::class]]; $this ->supportsDenormalization( @@ -65,11 +66,11 @@ final class CommandArgumentsDenormalizerSpec extends ObjectBehavior } function it_denormalizes_add_product_review_and_transforms_product_field_from_iri_to_code( - DenormalizerInterface $objectNormalizer, - iriToIdentifierConverterInterface $iriToIdentifierConverter, + DenormalizerInterface $commandDenormalizer, + IriToIdentifierConverterInterface $iriToIdentifierConverter, DataTransformerInterface $commandAwareInputDataTransformer, ): void { - $context['input']['class'] = AddProductReview::class; + $context = ['input' => ['class' => AddProductReview::class]]; $addProductReview = new AddProductReview('Cap', 5, 'ok', 'cap_code', 'john@example.com'); @@ -80,15 +81,15 @@ final class CommandArgumentsDenormalizerSpec extends ObjectBehavior $iriToIdentifierConverter->isIdentifier('/api/v2/shop/products/cap_code')->willReturn(true); $iriToIdentifierConverter->getIdentifier('/api/v2/shop/products/cap_code')->willReturn('cap_code'); - $objectNormalizer + $commandDenormalizer ->denormalize( [ - 'title' => 'Cap', - 'rating' => 5, - 'comment' => 'ok', - 'product' => 'cap_code', - 'email' => 'john@example.com', - ], + 'title' => 'Cap', + 'rating' => 5, + 'comment' => 'ok', + 'product' => 'cap_code', + 'email' => 'john@example.com', + ], AddProductReview::class, null, $context, @@ -108,12 +109,12 @@ final class CommandArgumentsDenormalizerSpec extends ObjectBehavior $this ->denormalize( [ - 'title' => 'Cap', - 'rating' => 5, - 'comment' => 'ok', - 'product' => '/api/v2/shop/products/cap_code', - 'email' => 'john@example.com', - ], + 'title' => 'Cap', + 'rating' => 5, + 'comment' => 'ok', + 'product' => '/api/v2/shop/products/cap_code', + 'email' => 'john@example.com', + ], AddProductReview::class, null, $context, @@ -121,4 +122,81 @@ final class CommandArgumentsDenormalizerSpec extends ObjectBehavior ->shouldReturn($addProductReview) ; } + + function it_denormalizes_a_command_with_an_array_of_iris( + DenormalizerInterface $commandDenormalizer, + IriToIdentifierConverterInterface $iriToIdentifierConverter, + DataTransformerInterface $commandAwareInputDataTransformer, + ): void { + $command = new class() implements IriToIdentifierConversionAwareInterface { + public string $iri = '/api/v2/iri'; + + public array $arrayIris = [ + '/api/v2/first-iri', + '/api/v2/second-iri', + ]; + + public array $arrayField = ['array']; + }; + $context = ['input' => ['class' => $command::class]]; + + $iriToIdentifierConverter->isIdentifier('array')->willReturn(false); + + $iriToIdentifierConverter->isIdentifier('/api/v2/iri')->willReturn(true); + $iriToIdentifierConverter->getIdentifier('/api/v2/iri')->willReturn('iri'); + + $iriToIdentifierConverter->isIdentifier('/api/v2/first-iri')->willReturn(true); + $iriToIdentifierConverter->getIdentifier('/api/v2/first-iri')->willReturn('first-iri'); + + $iriToIdentifierConverter->isIdentifier('')->willReturn(false); + $iriToIdentifierConverter->getIdentifier('')->shouldNotBeCalled(); + + $iriToIdentifierConverter->isIdentifier('/api/v2/second-iri')->willReturn(true); + $iriToIdentifierConverter->getIdentifier('/api/v2/second-iri')->willReturn('second-iri'); + + $commandDenormalizer + ->denormalize( + [ + 'iri' => 'iri', + 'arrayIris' => [ + 'first-iri', + 'second-iri', + '', + ], + 'arrayField' => ['array'], + ], + $command::class, + null, + $context, + ) + ->willReturn($command) + ; + + $commandAwareInputDataTransformer + ->supportsTransformation($command, $command::class, $context) + ->willReturn(false) + ; + $commandAwareInputDataTransformer + ->transform($command, $command::class, $context) + ->shouldNotBeCalled() + ; + + $this + ->denormalize( + [ + 'iri' => '/api/v2/iri', + 'arrayIris' => [ + '/api/v2/first-iri', + '/api/v2/second-iri', + '', + ], + 'arrayField' => ['array'], + ], + $command::class, + null, + $context, + ) + ->shouldReturn($command) + ; + } } diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandDenormalizerSpec.php index f84394f947..0a6460c5dc 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandDenormalizerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandDenormalizerSpec.php @@ -14,160 +14,22 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\ApiBundle\Serializer; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; use Sylius\Bundle\ApiBundle\Command\Account\RegisterShopUser; -use Sylius\Bundle\ApiBundle\Command\Account\VerifyCustomerAccount; -use Sylius\Component\Core\Model\Customer; +use Sylius\Bundle\ApiBundle\Exception\InvalidRequestArgumentException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; -use Symfony\Component\Serializer\NameConverter\NameConverterInterface; +use Symfony\Component\Serializer\Exception\NotNormalizableValueException; +use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; final class CommandDenormalizerSpec extends ObjectBehavior { - function let(DenormalizerInterface $baseNormalizer, NameConverterInterface $nameConverter): void + function let(DenormalizerInterface $baseNormalizer, AdvancedNameConverterInterface $nameConverter): void { $this->beConstructedWith($baseNormalizer, $nameConverter); } - function it_throws_exception_if_not_all_required_parameters_are_present_in_the_context( - DenormalizerInterface $baseNormalizer, - NameConverterInterface $nameConverter, - ): void { - $baseNormalizer->denormalize(Argument::any())->shouldNotBeCalled(); - - $nameConverter->normalize('firstName', RegisterShopUser::class)->willReturn('firstName'); - $nameConverter->normalize('lastName', RegisterShopUser::class)->willReturn('lastName'); - $nameConverter->normalize('email', RegisterShopUser::class)->willReturn('email'); - $nameConverter->normalize('password', RegisterShopUser::class)->willReturn('password'); - $nameConverter->normalize('subscribedToNewsletter', RegisterShopUser::class)->willReturn('subscribedToNewsletter'); - - $this - ->shouldThrow(new MissingConstructorArgumentsException( - 'Request does not have the following required fields specified: firstName, lastName.', - )) - ->during( - 'denormalize', - [ - ['email' => 'test@example.com', 'password' => 'pa$$word'], - '', - null, - ['input' => ['class' => RegisterShopUser::class]], - ], - ) - ; - } - - function it_denormalizes_data_if_all_required_parameters_are_specified( - DenormalizerInterface $baseNormalizer, - NameConverterInterface $nameConverter, - ): void { - $nameConverter->normalize('firstName', RegisterShopUser::class)->willReturn('firstName'); - $nameConverter->normalize('lastName', RegisterShopUser::class)->willReturn('lastName'); - $nameConverter->normalize('email', RegisterShopUser::class)->willReturn('email'); - $nameConverter->normalize('password', RegisterShopUser::class)->willReturn('password'); - $nameConverter->normalize('subscribedToNewsletter', RegisterShopUser::class)->willReturn('subscribedToNewsletter'); - - $baseNormalizer - ->denormalize( - ['firstName' => 'John', 'lastName' => 'Doe', 'email' => 'test@example.com', 'password' => 'pa$$word'], - Customer::class, - null, - ['input' => ['class' => RegisterShopUser::class]], - ) - ->willReturn(['key' => 'value']) - ; - - $this->denormalize( - ['firstName' => 'John', 'lastName' => 'Doe', 'email' => 'test@example.com', 'password' => 'pa$$word'], - Customer::class, - null, - ['input' => ['class' => RegisterShopUser::class]], - )->shouldReturn(['key' => 'value']); - } - - function it_denormalizes_data_if_all_required_parameters_are_specified_based_on_their_normalized_names( - DenormalizerInterface $baseNormalizer, - NameConverterInterface $nameConverter, - ): void { - $baseNormalizer - ->denormalize( - ['first_name' => 'John', 'last_name' => 'Doe', 'email_address' => 'test@example.com', 'pass' => 'pa$$word'], - Customer::class, - null, - ['input' => ['class' => RegisterShopUser::class]], - ) - ->willReturn(['key' => 'value']) - ; - - $nameConverter->normalize('firstName', RegisterShopUser::class)->willReturn('first_name'); - $nameConverter->normalize('lastName', RegisterShopUser::class)->willReturn('last_name'); - $nameConverter->normalize('email', RegisterShopUser::class)->willReturn('email_address'); - $nameConverter->normalize('password', RegisterShopUser::class)->willReturn('pass'); - $nameConverter->normalize('subscribedToNewsletter', RegisterShopUser::class)->willReturn('hasNewsletter'); - - $this->denormalize( - ['first_name' => 'John', 'last_name' => 'Doe', 'email_address' => 'test@example.com', 'pass' => 'pa$$word'], - Customer::class, - null, - ['input' => ['class' => RegisterShopUser::class]], - )->shouldReturn(['key' => 'value']); - } - - function it_does_not_check_parameters_if_there_is_an_object_to_populate( - DenormalizerInterface $baseNormalizer, - ): void { - $baseNormalizer - ->denormalize( - [], - Customer::class, - null, - [ - 'input' => ['class' => VerifyCustomerAccount::class], - 'object_to_populate' => new VerifyCustomerAccount('TOKEN'), - ], - ) - ->willReturn(['key' => 'value']) - ; - - $this - ->denormalize( - [], - Customer::class, - null, - [ - 'input' => ['class' => VerifyCustomerAccount::class], - 'object_to_populate' => new VerifyCustomerAccount('TOKEN'), - ], - ) - ->shouldReturn(['key' => 'value']) - ; - } - - function it_does_not_check_parameters_if_there_is_no_constructor( - DenormalizerInterface $baseNormalizer, - ): void { - $baseNormalizer - ->denormalize( - [], - Customer::class, - null, - ['input' => ['class' => \stdClass::class]], - ) - ->willReturn(['key' => 'value']) - ; - - $this - ->denormalize( - [], - Customer::class, - null, - ['input' => ['class' => \stdClass::class]], - ) - ->shouldReturn(['key' => 'value']) - ; - } - function it_implements_context_aware_denormalizer_interface(): void { $this->shouldImplement(ContextAwareDenormalizerInterface::class); @@ -175,11 +37,68 @@ final class CommandDenormalizerSpec extends ObjectBehavior function it_supports_denormalization_for_specified_input_class(): void { - $this->supportsDenormalization(null, '', null, ['input' => ['class' => 'Class']])->shouldReturn(true); + $this->supportsDenormalization(null, '', context: ['input' => ['class' => 'Class']])->shouldReturn(true); } function it_does_not_support_denormalization_for_not_specified_input_class(): void { - $this->supportsDenormalization(null, '', null, [])->shouldReturn(false); + $this->supportsDenormalization(null, '')->shouldReturn(false); + } + + function it_throws_exception_if_not_all_required_parameters_are_present_in_the_context( + DenormalizerInterface $baseNormalizer, + AdvancedNameConverterInterface $nameConverter, + ): void { + $exception = new MissingConstructorArgumentsException('', 400, null, ['firstName', 'lastName']); + $context = ['input' => ['class' => RegisterShopUser::class]]; + $data = ['email' => 'test@example.com', 'password' => 'pa$$word']; + + $nameConverter->normalize('firstName', class: RegisterShopUser::class)->willReturn('first_name'); + $nameConverter->normalize('lastName', class: RegisterShopUser::class)->willReturn('lastName'); + + $baseNormalizer->denormalize($data, '', null, $context)->willThrow($exception); + + $this + ->shouldThrow(new MissingConstructorArgumentsException( + 'Request does not have the following required fields specified: first_name, lastName.', + )) + ->during('denormalize', [$data, '', null, $context]) + ; + } + + function it_throws_exception_for_mismatched_argument_type( + DenormalizerInterface $baseNormalizer, + AdvancedNameConverterInterface $nameConverter, + ): void { + $previousException = NotNormalizableValueException::createForUnexpectedDataType('', 1, ['string'], 'firstName'); + $exception = new UnexpectedValueException('', 400, $previousException); + $context = ['input' => ['class' => RegisterShopUser::class]]; + $data = ['firstName' => 1]; + + $nameConverter->normalize('firstName', class: RegisterShopUser::class)->willReturn('first_name'); + + $baseNormalizer->denormalize($data, '', null, $context)->willThrow($exception); + + $this + ->shouldThrow(new InvalidRequestArgumentException( + 'Request field "first_name" should be of type "string".', + )) + ->during('denormalize', [$data, '', null, $context]) + ; + } + + function it_throws_the_same_exception_if_previous_exception_is_not_normalizable_value_exception( + DenormalizerInterface $baseNormalizer, + ): void { + $exception = new UnexpectedValueException('Unexpected value'); + $context = ['input' => ['class' => RegisterShopUser::class]]; + $data = ['firstName' => '1']; + + $baseNormalizer->denormalize($data, '', null, $context)->willThrow($exception); + + $this + ->shouldThrow(new UnexpectedValueException('Unexpected value')) + ->during('denormalize', [$data, '', null, $context]) + ; } } diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandNormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandNormalizerSpec.php index c66a9b72e8..a0dfee0a7e 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandNormalizerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/CommandNormalizerSpec.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\ApiBundle\Serializer; use PhpSpec\ObjectBehavior; +use Sylius\Bundle\ApiBundle\Exception\InvalidRequestArgumentException; use Symfony\Component\Serializer\Exception\MissingConstructorArgumentsException; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -40,6 +41,15 @@ final class CommandNormalizerSpec extends ObjectBehavior } }, )->shouldReturn(true); + + $this->supportsNormalization( + new class() { + public function getClass(): string + { + return InvalidRequestArgumentException::class; + } + }, + )->shouldReturn(true); } function it_does_not_support_normalization_if_data_has_no_get_class_method(): void diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/CustomerDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/CustomerDenormalizerSpec.php new file mode 100644 index 0000000000..3484d8160f --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/CustomerDenormalizerSpec.php @@ -0,0 +1,84 @@ +beConstructedWith($calendar); + } + + function it_does_not_support_denormalization_when_the_denormalizer_has_already_been_called(): void + { + $this->supportsDenormalization([], CustomerInterface::class, context: [self::ALREADY_CALLED => true])->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_data_is_not_an_array(): void + { + $this->supportsDenormalization('string', CustomerInterface::class)->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_type_is_not_a_customer(): void + { + $this->supportsDenormalization([], 'string')->shouldReturn(false); + } + + function it_does_nothing_if_user_verified_is_not_set( + DenormalizerInterface $denormalizer, + DateTimeProviderInterface $calendar, + CustomerInterface $customer, + ): void { + $this->setDenormalizer($denormalizer); + $denormalizer->denormalize([], CustomerInterface::class, null, [self::ALREADY_CALLED => true])->willReturn($customer); + + $this->denormalize([], CustomerInterface::class)->shouldReturn($customer); + + $calendar->now()->shouldNotHaveBeenCalled(); + } + + public function it_changes_user_verified_from_false_to_null( + DenormalizerInterface $denormalizer, + DateTimeProviderInterface $calendar, + CustomerInterface $customer, + ): void { + $this->setDenormalizer($denormalizer); + $denormalizer->denormalize(['user' => ['verified' => null]], CustomerInterface::class, null, [self::ALREADY_CALLED => true])->willReturn($customer); + + $this->denormalize(['user' => ['verified' => false]], CustomerInterface::class)->shouldReturn($customer); + + $calendar->now()->shouldNotHaveBeenCalled(); + } + + public function it_changes_user_verified_from_true_to_datetime( + DenormalizerInterface $denormalizer, + DateTimeProviderInterface $calendar, + CustomerInterface $customer, + ): void { + $this->setDenormalizer($denormalizer); + + $dateTime = new \DateTime('2021-01-01T00:00:00+00:00'); + $calendar->now()->willReturn($dateTime); + $denormalizer->denormalize(['user' => ['verified' => '2021-01-01T00:00:00+00:00']], CustomerInterface::class, null, [self::ALREADY_CALLED => true])->willReturn($customer); + + $this->denormalize(['user' => ['verified' => true]], CustomerInterface::class)->shouldReturn($customer); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/DoctrineCollectionValuesNormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/DoctrineCollectionValuesNormalizerSpec.php new file mode 100644 index 0000000000..6ed0c4aa2c --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/DoctrineCollectionValuesNormalizerSpec.php @@ -0,0 +1,48 @@ +supportsNormalization($order)->shouldReturn(false); + $this->supportsNormalization($order, null, ['collection_values' => false])->shouldReturn(false); + $this->supportsNormalization($order, null, ['collection_values' => true])->shouldReturn(false); + + $this->supportsNormalization($collection)->shouldReturn(false); + $this->supportsNormalization($collection, null, ['collection_values' => false])->shouldReturn(false); + $this->supportsNormalization($collection, null, ['collection_values' => true])->shouldReturn(true); + } + + function it_normalizes_collection_values( + NormalizerInterface $normalizer, + ): void { + $this->setNormalizer($normalizer); + + $collection = new ArrayCollection(['1' => ['id' => 1], '2' => ['id' => 2]]); + + $this->normalize($collection, null, ['collection_values' => true]); + + $normalizer->normalize([['id' => 1], ['id' => 2]], null, ['collection_values' => true])->shouldHaveBeenCalled(); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductAttributeValueDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductAttributeValueDenormalizerSpec.php new file mode 100644 index 0000000000..21bd22e96e --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductAttributeValueDenormalizerSpec.php @@ -0,0 +1,101 @@ +beConstructedWith($iriConverter); + } + + function it_does_not_support_denormalization_when_the_denormalizer_has_already_been_called(): void + { + $this + ->supportsDenormalization([], ProductAttributeValueInterface::class, context: [self::ALREADY_CALLED => true]) + ->shouldReturn(false) + ; + } + + function it_does_not_support_denormalization_when_data_is_not_an_array(): void + { + $this->supportsDenormalization('string', ProductAttributeValueInterface::class)->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_type_is_not_a_product_attribute_value(): void + { + $this->supportsDenormalization([], 'string')->shouldReturn(false); + } + + function it_throws_an_exception_if_given_value_is_in_wrong_type( + IriConverterInterface $iriConverter, + DenormalizerInterface $denormalizer, + ProductAttributeInterface $attribute, + ): void { + $iriConverter->getResourceFromIri('/attributes/material')->willReturn($attribute); + + $attribute->getStorageType()->willReturn('text'); + $attribute->getName()->willReturn('Material'); + + $this->setDenormalizer($denormalizer); + $denormalizer + ->denormalize([], ProductAttributeValueInterface::class, null, [self::ALREADY_CALLED => true]) + ->shouldNotBeCalled() + ; + + $this + ->shouldThrow(InvalidProductAttributeValueTypeException::class) + ->during('denormalize', [ + ['attribute' => '/attributes/material', 'value' => 4], + ProductAttributeValueInterface::class, + ]) + ; + } + + function it_denormalizes_data_if_given_value_is_in_proper_types( + IriConverterInterface $iriConverter, + DenormalizerInterface $denormalizer, + ProductAttributeInterface $attribute, + ): void { + $iriConverter->getResourceFromIri('/attributes/material')->willReturn($attribute); + + $attribute->getStorageType()->willReturn('text'); + $attribute->getType()->willReturn('text'); + + $this->setDenormalizer($denormalizer); + $denormalizer + ->denormalize( + ['attribute' => '/attributes/material', 'value' => 'ceramic'], + ProductAttributeValueInterface::class, + null, + [self::ALREADY_CALLED => true], + ) + ->shouldBeCalled() + ; + + $this->denormalize( + ['attribute' => '/attributes/material', 'value' => 'ceramic'], + ProductAttributeValueInterface::class, + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductDenormalizerSpec.php new file mode 100644 index 0000000000..947c1eaca2 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductDenormalizerSpec.php @@ -0,0 +1,136 @@ +supportsDenormalization([], ProductInterface::class, context: [self::ALREADY_CALLED => true]) + ->shouldReturn(false) + ; + } + + function it_does_not_support_denormalization_when_data_is_not_an_array(): void + { + $this->supportsDenormalization('string', ProductInterface::class)->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_type_is_not_a_product(): void + { + $this->supportsDenormalization([], 'string')->shouldReturn(false); + } + + function it_removes_options_from_data_if_given_product_has_variants_defined( + DenormalizerInterface $denormalizer, + ProductInterface $product, + ProductVariantInterface $productVariant, + ): void { + $product->getVariants()->willReturn(new ArrayCollection([$productVariant->getWrappedObject()])); + + $this->setDenormalizer($denormalizer); + $denormalizer + ->denormalize( + [], + ProductInterface::class, + null, + [ + AbstractNormalizer::OBJECT_TO_POPULATE => $product, + self::ALREADY_CALLED => true, + ], + ) + ->shouldBeCalled() + ; + + $this->denormalize( + ['options' => ['/options/color']], + ProductInterface::class, + null, + [AbstractNormalizer::OBJECT_TO_POPULATE => $product], + ); + } + + function it_does_not_remove_options_from_data_if_given_product_has_np_variants_defined( + DenormalizerInterface $denormalizer, + ProductInterface $product, + ): void { + $product->getVariants()->willReturn(new ArrayCollection([])); + + $this->setDenormalizer($denormalizer); + $denormalizer + ->denormalize( + ['options' => ['/options/color']], + ProductInterface::class, + null, + [ + AbstractNormalizer::OBJECT_TO_POPULATE => $product, + self::ALREADY_CALLED => true, + ], + ) + ->shouldBeCalled() + ; + + $this->denormalize( + ['options' => ['/options/color']], + ProductInterface::class, + null, + [AbstractNormalizer::OBJECT_TO_POPULATE => $product], + ); + } + + function it_does_not_remove_options_from_data_if_there_is_no_object_to_populate_in_context_defined( + DenormalizerInterface $denormalizer, + ): void { + $this->setDenormalizer($denormalizer); + $denormalizer + ->denormalize( + ['options' => ['/options/color']], + ProductInterface::class, + null, + [self::ALREADY_CALLED => true], + ) + ->shouldBeCalled() + ; + + $this->denormalize(['options' => ['/options/color']], ProductInterface::class); + } + + function it_throws_an_exception_if_object_to_populate_is_not_a_product( + DenormalizerInterface $denormalizer, + ProductVariantInterface $productVariant, + ): void { + $this->setDenormalizer($denormalizer); + $denormalizer->denormalize([], ProductInterface::class, null, [self::ALREADY_CALLED => true])->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('denormalize', [ + ['options' => ['/options/color']], + ProductInterface::class, + null, + [AbstractNormalizer::OBJECT_TO_POPULATE => $productVariant], + ]) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductNormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductNormalizerSpec.php index dd12a04e3a..7b89a3cc14 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductNormalizerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductNormalizerSpec.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\ApiBundle\Serializer; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Doctrine\Common\Collections\ArrayCollection; use PhpSpec\ObjectBehavior; use Sylius\Component\Core\Model\OrderInterface; @@ -80,7 +80,7 @@ final class ProductNormalizerSpec extends ObjectBehavior $normalizer->normalize($product, null, ['sylius_product_normalizer_already_called' => true])->willReturn([]); $product->getEnabledVariants()->willReturn(new ArrayCollection([$variant->getWrappedObject()])); $defaultProductVariantResolver->getVariant($product)->willReturn($variant); - $iriConverter->getIriFromItem($variant)->willReturn('/api/v2/shop/product-variants/CODE'); + $iriConverter->getIriFromResource($variant)->willReturn('/api/v2/shop/product-variants/CODE'); $this->normalize($product, null, [])->shouldReturn([ 'variants' => ['/api/v2/shop/product-variants/CODE'], @@ -98,7 +98,7 @@ final class ProductNormalizerSpec extends ObjectBehavior $this->setNormalizer($normalizer); $normalizer->normalize($product, null, ['sylius_product_normalizer_already_called' => true])->willReturn([]); - $iriConverter->getIriFromItem($variant)->willReturn('/api/v2/shop/product-variants/CODE'); + $iriConverter->getIriFromResource($variant)->willReturn('/api/v2/shop/product-variants/CODE'); $product->getEnabledVariants()->willReturn(new ArrayCollection([$variant->getWrappedObject()])); $defaultProductVariantResolver->getVariant($product)->willReturn(null); diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductVariantChannelPricingsChannelCodeKeyDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductVariantChannelPricingsChannelCodeKeyDenormalizerSpec.php new file mode 100644 index 0000000000..3c99024ad6 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductVariantChannelPricingsChannelCodeKeyDenormalizerSpec.php @@ -0,0 +1,82 @@ +supportsDenormalization([], ProductVariantInterface::class, context: [ + 'sylius_product_variant_channel_pricings_channel_code_key_denormalizer_already_called' => true, + ])->shouldReturn(false) + ; + } + + function it_does_not_support_denormalization_when_data_is_not_an_array(): void + { + $this->supportsDenormalization('string', ProductVariantInterface::class)->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_type_is_not_a_product_variant(): void + { + $this->supportsDenormalization([], 'string')->shouldReturn(false); + } + + function it_does_nothing_if_there_is_no_channel_pricings_key( + DenormalizerInterface $denormalizer, + ): void { + $this->setDenormalizer($denormalizer); + + $this->denormalize([], ProductVariantInterface::class); + + $denormalizer->denormalize([], ProductVariantInterface::class, null, [ + 'sylius_product_variant_channel_pricings_channel_code_key_denormalizer_already_called' => true, + ])->shouldHaveBeenCalledOnce(); + } + + function it_changes_keys_of_channel_pricings_to_channel_code( + DenormalizerInterface $denormalizer, + ): void { + $this->setDenormalizer($denormalizer); + + $originalData = ['channelPricings' => ['WEB' => ['channelCode' => 'WEB'], 'MOBILE' => []]]; + $updatedData = ['channelPricings' => ['WEB' => ['channelCode' => 'WEB'], 'MOBILE' => ['channelCode' => 'MOBILE']]]; + + $this->denormalize($originalData, ProductVariantInterface::class); + + $denormalizer->denormalize( + $updatedData, + ProductVariantInterface::class, + null, + ['sylius_product_variant_channel_pricings_channel_code_key_denormalizer_already_called' => true], + )->shouldHaveBeenCalledOnce(); + } + + function it_throws_an_exception_if_channel_code_is_not_the_same_as_key( + DenormalizerInterface $denormalizer, + ): void { + $this->setDenormalizer($denormalizer); + + $this + ->shouldThrow(ChannelPricingChannelCodeMismatchException::class) + ->during('denormalize', [['channelPricings' => ['WEB' => ['channelCode' => 'MOBILE']]], ProductVariantInterface::class]) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductVariantNormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductVariantNormalizerSpec.php index a41f820e42..5f8c5ccc56 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductVariantNormalizerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ProductVariantNormalizerSpec.php @@ -13,15 +13,14 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\ApiBundle\Serializer; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Doctrine\Common\Collections\ArrayCollection; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Sylius\Bundle\ApiBundle\SectionResolver\AdminApiSection; use Sylius\Bundle\ApiBundle\SectionResolver\ShopApiSection; +use Sylius\Bundle\ApiBundle\Serializer\ContextKeys; use Sylius\Bundle\CoreBundle\SectionResolver\SectionProviderInterface; -use Sylius\Component\Channel\Context\ChannelContextInterface; -use Sylius\Component\Channel\Context\ChannelNotFoundException; use Sylius\Component\Core\Calculator\ProductVariantPricesCalculatorInterface; use Sylius\Component\Core\Exception\MissingChannelConfigurationException; use Sylius\Component\Core\Model\CatalogPromotionInterface; @@ -35,12 +34,11 @@ final class ProductVariantNormalizerSpec extends ObjectBehavior { function let( ProductVariantPricesCalculatorInterface $pricesCalculator, - ChannelContextInterface $channelContext, AvailabilityCheckerInterface $availabilityChecker, SectionProviderInterface $sectionProvider, IriConverterInterface $iriConverter, ): void { - $this->beConstructedWith($pricesCalculator, $channelContext, $availabilityChecker, $sectionProvider, $iriConverter); + $this->beConstructedWith($pricesCalculator, $availabilityChecker, $sectionProvider, $iriConverter); } function it_supports_only_product_variant_interface(ProductVariantInterface $variant, OrderInterface $order): void @@ -77,7 +75,6 @@ final class ProductVariantNormalizerSpec extends ObjectBehavior function it_serializes_product_variant_if_item_operation_name_is_different_that_admin_get( ProductVariantPricesCalculatorInterface $pricesCalculator, - ChannelContextInterface $channelContext, AvailabilityCheckerInterface $availabilityChecker, NormalizerInterface $normalizer, ChannelInterface $channel, @@ -85,22 +82,27 @@ final class ProductVariantNormalizerSpec extends ObjectBehavior ): void { $this->setNormalizer($normalizer); - $normalizer->normalize($variant, null, ['sylius_product_variant_normalizer_already_called' => true])->willReturn([]); + $normalizer->normalize($variant, null, [ + 'sylius_product_variant_normalizer_already_called' => true, + ContextKeys::CHANNEL => $channel, + ])->willReturn([]); - $channelContext->getChannel()->willReturn($channel); $pricesCalculator->calculate($variant, ['channel' => $channel])->willReturn(1000); $pricesCalculator->calculateOriginal($variant, ['channel' => $channel])->willReturn(1000); + $pricesCalculator->calculateLowestPriceBeforeDiscount($variant, ['channel' => $channel])->willReturn(500); $variant->getAppliedPromotionsForChannel($channel)->willReturn(new ArrayCollection()); $availabilityChecker->isStockAvailable($variant)->willReturn(true); - $this->normalize($variant, null, [])->shouldBeLike(['price' => 1000, 'originalPrice' => 1000, 'inStock' => true]); + $this + ->normalize($variant, null, [ContextKeys::CHANNEL => $channel]) + ->shouldBeLike(['price' => 1000, 'originalPrice' => 1000, 'lowestPriceBeforeDiscount' => 500, 'inStock' => true]) + ; } function it_returns_original_price_if_is_different_than_price( ProductVariantPricesCalculatorInterface $pricesCalculator, - ChannelContextInterface $channelContext, AvailabilityCheckerInterface $availabilityChecker, NormalizerInterface $normalizer, ChannelInterface $channel, @@ -108,22 +110,27 @@ final class ProductVariantNormalizerSpec extends ObjectBehavior ): void { $this->setNormalizer($normalizer); - $normalizer->normalize($variant, null, ['sylius_product_variant_normalizer_already_called' => true])->willReturn([]); + $normalizer->normalize($variant, null, [ + 'sylius_product_variant_normalizer_already_called' => true, + ContextKeys::CHANNEL => $channel, + ])->willReturn([]); - $channelContext->getChannel()->willReturn($channel); $pricesCalculator->calculate($variant, ['channel' => $channel])->willReturn(500); $pricesCalculator->calculateOriginal($variant, ['channel' => $channel])->willReturn(1000); + $pricesCalculator->calculateLowestPriceBeforeDiscount($variant, ['channel' => $channel])->willReturn(100); $variant->getAppliedPromotionsForChannel($channel)->willReturn(new ArrayCollection()); $availabilityChecker->isStockAvailable($variant)->willReturn(true); - $this->normalize($variant, null, [])->shouldBeLike(['price' => 500, 'originalPrice' => 1000, 'inStock' => true]); + $this + ->normalize($variant, null, [ContextKeys::CHANNEL => $channel]) + ->shouldBeLike(['price' => 500, 'originalPrice' => 1000, 'lowestPriceBeforeDiscount' => 100, 'inStock' => true]) + ; } function it_returns_catalog_promotions_if_applied( ProductVariantPricesCalculatorInterface $pricesCalculator, - ChannelContextInterface $channelContext, AvailabilityCheckerInterface $availabilityChecker, NormalizerInterface $normalizer, ChannelInterface $channel, @@ -133,31 +140,34 @@ final class ProductVariantNormalizerSpec extends ObjectBehavior ): void { $this->setNormalizer($normalizer); - $normalizer->normalize($variant, null, ['sylius_product_variant_normalizer_already_called' => true])->willReturn([]); + $normalizer->normalize($variant, null, [ + 'sylius_product_variant_normalizer_already_called' => true, + ContextKeys::CHANNEL => $channel, + ])->willReturn([]); - $channelContext->getChannel()->willReturn($channel); $pricesCalculator->calculate($variant, ['channel' => $channel])->willReturn(500); $pricesCalculator->calculateOriginal($variant, ['channel' => $channel])->willReturn(1000); + $pricesCalculator->calculateLowestPriceBeforeDiscount($variant, ['channel' => $channel])->willReturn(100); $catalogPromotion->getCode()->willReturn('winter_sale'); $variant->getAppliedPromotionsForChannel($channel)->willReturn(new ArrayCollection([$catalogPromotion->getWrappedObject()])); $availabilityChecker->isStockAvailable($variant)->willReturn(true); - $iriConverter->getIriFromItem($catalogPromotion)->willReturn('/api/v2/shop/catalog-promotions/winter_sale'); + $iriConverter->getIriFromResource($catalogPromotion)->willReturn('/api/v2/shop/catalog-promotions/winter_sale'); $this - ->normalize($variant) + ->normalize($variant, null, [ContextKeys::CHANNEL => $channel]) ->shouldBeLike([ 'price' => 500, 'originalPrice' => 1000, + 'lowestPriceBeforeDiscount' => 100, 'appliedPromotions' => ['/api/v2/shop/catalog-promotions/winter_sale'], 'inStock' => true, ]) ; } - function it_doesnt_return_prices_and_promotions_when_channel_is_not_found( + function it_doesnt_return_prices_and_promotions_when_channel_key_is_not_in_the_context( ProductVariantPricesCalculatorInterface $pricesCalculator, - ChannelContextInterface $channelContext, AvailabilityCheckerInterface $availabilityChecker, NormalizerInterface $normalizer, ProductVariantInterface $variant, @@ -166,8 +176,6 @@ final class ProductVariantNormalizerSpec extends ObjectBehavior $normalizer->normalize($variant, null, ['sylius_product_variant_normalizer_already_called' => true])->willReturn([]); - $channelContext->getChannel()->willThrow(ChannelNotFoundException::class); - $pricesCalculator->calculate(Argument::cetera())->shouldNotBeCalled(); $pricesCalculator->calculateOriginal(Argument::cetera())->shouldNotBeCalled(); $variant->getAppliedPromotionsForChannel(Argument::any())->shouldNotBeCalled(); @@ -177,9 +185,30 @@ final class ProductVariantNormalizerSpec extends ObjectBehavior $this->normalize($variant, null, [])->shouldReturn(['inStock' => true]); } + function it_doesnt_return_prices_and_promotions_when_channel_from_context_is_null( + ProductVariantPricesCalculatorInterface $pricesCalculator, + AvailabilityCheckerInterface $availabilityChecker, + NormalizerInterface $normalizer, + ProductVariantInterface $variant, + ): void { + $this->setNormalizer($normalizer); + + $normalizer->normalize($variant, null, [ + 'sylius_product_variant_normalizer_already_called' => true, + ContextKeys::CHANNEL => null, + ])->willReturn([]); + + $pricesCalculator->calculate(Argument::cetera())->shouldNotBeCalled(); + $pricesCalculator->calculateOriginal(Argument::cetera())->shouldNotBeCalled(); + $variant->getAppliedPromotionsForChannel(Argument::any())->shouldNotBeCalled(); + + $availabilityChecker->isStockAvailable($variant)->willReturn(true); + + $this->normalize($variant, null, [ContextKeys::CHANNEL => null])->shouldReturn(['inStock' => true]); + } + function it_doesnt_return_prices_if_channel_configuration_is_not_found( ProductVariantPricesCalculatorInterface $pricesCalculator, - ChannelContextInterface $channelContext, AvailabilityCheckerInterface $availabilityChecker, NormalizerInterface $normalizer, ChannelInterface $channel, @@ -187,9 +216,11 @@ final class ProductVariantNormalizerSpec extends ObjectBehavior ): void { $this->setNormalizer($normalizer); - $normalizer->normalize($variant, null, ['sylius_product_variant_normalizer_already_called' => true])->willReturn([]); + $normalizer->normalize($variant, null, [ + 'sylius_product_variant_normalizer_already_called' => true, + ContextKeys::CHANNEL => $channel, + ])->willReturn([]); - $channelContext->getChannel()->willReturn($channel); $pricesCalculator->calculate($variant, ['channel' => $channel])->willThrow(MissingChannelConfigurationException::class); $pricesCalculator->calculateOriginal($variant, ['channel' => $channel])->willThrow(MissingChannelConfigurationException::class); @@ -197,7 +228,7 @@ final class ProductVariantNormalizerSpec extends ObjectBehavior $availabilityChecker->isStockAvailable($variant)->willReturn(true); - $this->normalize($variant, null, [])->shouldReturn(['inStock' => true]); + $this->normalize($variant, null, [ContextKeys::CHANNEL => $channel])->shouldReturn(['inStock' => true]); } function it_throws_an_exception_if_the_normalizer_has_been_already_called( diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/TaxRateDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TaxRateDenormalizerSpec.php new file mode 100644 index 0000000000..ffd47cee7b --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TaxRateDenormalizerSpec.php @@ -0,0 +1,74 @@ +setDenormalizer($denormalizer); + } + + function it_supports_denormalization_of_tax_rate_with_amount_set(): void + { + $this + ->supportsDenormalization(['amount' => 0.23], \stdClass::class) + ->shouldReturn(false) + ; + $this + ->supportsDenormalization(0.23, TaxRateInterface::class) + ->shouldReturn(false) + ; + $this + ->supportsDenormalization([], TaxRateInterface::class) + ->shouldReturn(false) + ; + $this + ->supportsDenormalization(['amount' => 0.23], TaxRateInterface::class, null, [self::ALREADY_CALLED => true]) + ->shouldReturn(false) + ; + $this + ->supportsDenormalization(['amount' => 0.23], TaxRateInterface::class) + ->shouldReturn(true) + ; + } + + function it_denormalizes_tax_rate_changing_float_amount_to_string( + DenormalizerInterface $denormalizer, + TaxRateInterface $taxRate, + ): void { + $denormalizer->denormalize(['amount' => '0.23'], TaxRateInterface::class, null, [self::ALREADY_CALLED => true]) + ->willReturn($taxRate) + ; + + $this->denormalize(['amount' => 0.23], TaxRateInterface::class)->shouldReturn($taxRate); + } + + function it_denormalizes_tax_rate_changing_int_amount_to_string( + DenormalizerInterface $denormalizer, + TaxRateInterface $taxRate, + ): void { + $denormalizer->denormalize(['amount' => '12'], TaxRateInterface::class, null, [self::ALREADY_CALLED => true]) + ->willReturn($taxRate) + ; + + $this->denormalize(['amount' => 12], TaxRateInterface::class)->shouldReturn($taxRate); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableDenormalizerSpec.php new file mode 100644 index 0000000000..6da72ab875 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableDenormalizerSpec.php @@ -0,0 +1,186 @@ +beConstructedWith($localeProvider); + + $this->setDenormalizer($denormalizer); + } + + function it_only_supports_translatable_resource(): void + { + $this->supportsDenormalization([], TranslatableInterface::class, null, [ + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'PUT', + ])->shouldReturn(false); + + $this->supportsDenormalization([], TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldReturn(false); + + $this->supportsDenormalization([], \stdClass::class, null, [ + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldReturn(false); + } + + function it_does_nothing_when_data_contains_a_translation_in_default_locale( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $data = ['translations' => ['en' => ['locale' => 'en']]]; + + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $denormalizer->denormalize($data, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($data); + + $this + ->denormalize($data, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($data) + ; + } + + function it_adds_default_translation_when_no_translations_passed_in_data( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $updatedData = ['translations' => ['en' => ['locale' => 'en']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize([], TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } + + function it_adds_default_translation_when_no_translation_passed_for_default_locale_in_data( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $originalData = ['translations' => ['en' => []]]; + $updatedData = ['translations' => ['en' => ['locale' => 'en']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } + + function it_adds_default_translation_when_passed_default_translation_has_empty_locale( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $originalData = ['translations' => ['en' => ['locale' => '']]]; + $updatedData = ['translations' => ['en' => ['locale' => 'en']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } + + function it_adds_default_translation_when_passed_default_translation_has_null_locale( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $originalData = ['translations' => ['en' => ['locale' => null]]]; + $updatedData = ['translations' => ['en' => ['locale' => 'en']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } + + function it_adds_default_translation_when_passed_default_translation_has_mismatched_locale( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $originalData = ['translations' => ['en' => ['locale' => 'fr']]]; + $updatedData = ['translations' => ['en' => ['locale' => 'en']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } + + function it_adds_default_translation_when_no_translation_in_default_locale_passed_in_data( + DenormalizerInterface $denormalizer, + TranslationLocaleProviderInterface $localeProvider, + ): void { + $localeProvider->getDefaultLocaleCode()->willReturn('en'); + + $originalData = ['translations' => ['pl' => ['locale' => 'pl']]]; + $updatedData = ['translations' => ['en' => ['locale' => 'en'], 'pl' => ['locale' => 'pl']]]; + + $denormalizer->denormalize($updatedData, TranslatableInterface::class, null, [ + 'sylius_translatable_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST', + ])->shouldBeCalled()->willReturn($updatedData); + + $this + ->denormalize($originalData, TranslatableInterface::class, null, [ContextKeys::HTTP_REQUEST_METHOD_TYPE => 'POST']) + ->shouldReturn($updatedData) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableLocaleKeyDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableLocaleKeyDenormalizerSpec.php new file mode 100644 index 0000000000..ef55707d59 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/TranslatableLocaleKeyDenormalizerSpec.php @@ -0,0 +1,82 @@ +supportsDenormalization([], TranslatableInterface::class, context: [ + 'sylius_translatable_locale_key_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ])->shouldReturn(false) + ; + } + + function it_does_not_support_denormalization_when_data_is_not_an_array(): void + { + $this->supportsDenormalization('string', TranslatableInterface::class)->shouldReturn(false); + } + + function it_does_not_support_denormalization_when_type_is_not_a_translatable(): void + { + $this->supportsDenormalization([], 'string')->shouldReturn(false); + } + + function it_does_nothing_if_there_is_no_translation_key( + DenormalizerInterface $denormalizer, + ): void { + $this->setDenormalizer($denormalizer); + + $this->denormalize([], TranslatableInterface::class); + + $denormalizer->denormalize([], TranslatableInterface::class, null, [ + 'sylius_translatable_locale_key_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true, + ])->shouldHaveBeenCalledOnce(); + } + + function it_changes_keys_of_translations_to_locale( + DenormalizerInterface $denormalizer, + ): void { + $this->setDenormalizer($denormalizer); + + $originalData = ['translations' => ['en_US' => ['locale' => 'en_US'], 'de_DE' => []]]; + $updatedData = ['translations' => ['en_US' => ['locale' => 'en_US'], 'de_DE' => ['locale' => 'de_DE']]]; + + $this->denormalize($originalData, TranslatableInterface::class); + + $denormalizer->denormalize( + $updatedData, + TranslatableInterface::class, + null, + ['sylius_translatable_locale_key_denormalizer_already_called_for_Sylius\Component\Resource\Model\TranslatableInterface' => true], + )->shouldHaveBeenCalledOnce(); + } + + function it_throws_an_exception_if_locale_is_not_the_same_as_key( + DenormalizerInterface $denormalizer, + ): void { + $this->setDenormalizer($denormalizer); + + $this + ->shouldThrow(TranslationLocaleMismatchException::class) + ->during('denormalize', [['translations' => ['de_DE' => ['locale' => 'en_US']]], TranslatableInterface::class]) + ; + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ZoneDenormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ZoneDenormalizerSpec.php index adc00f09ca..dd2019d106 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Serializer/ZoneDenormalizerSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Serializer/ZoneDenormalizerSpec.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\ApiBundle\Serializer; -use ApiPlatform\Core\Api\IriConverterInterface; +use ApiPlatform\Api\IriConverterInterface; use Doctrine\Common\Collections\ArrayCollection; use PhpSpec\ObjectBehavior; use Sylius\Bundle\ApiBundle\Serializer\ZoneDenormalizer; @@ -108,9 +108,9 @@ final class ZoneDenormalizerSpec extends ObjectBehavior $franceZone->getWrappedObject(), ])); - $iriConverter->getIriFromItem($belgiumZone)->willReturn('iri/EU-BE'); - $iriConverter->getIriFromItem($germanyZone)->willReturn('iri/EU-DE'); - $iriConverter->getIriFromItem($franceZone)->willReturn('iri/EU-FR'); + $iriConverter->getIriFromResource($belgiumZone)->willReturn('iri/EU-BE'); + $iriConverter->getIriFromResource($germanyZone)->willReturn('iri/EU-DE'); + $iriConverter->getIriFromResource($franceZone)->willReturn('iri/EU-FR'); $denormalizer->denormalize( [ @@ -163,9 +163,9 @@ final class ZoneDenormalizerSpec extends ObjectBehavior $franceZone->getWrappedObject(), ])); - $iriConverter->getIriFromItem($belgiumZone)->shouldNotBeCalled(); - $iriConverter->getIriFromItem($germanyZone)->shouldNotBeCalled(); - $iriConverter->getIriFromItem($franceZone)->shouldNotBeCalled(); + $iriConverter->getIriFromResource($belgiumZone)->shouldNotBeCalled(); + $iriConverter->getIriFromResource($germanyZone)->shouldNotBeCalled(); + $iriConverter->getIriFromResource($franceZone)->shouldNotBeCalled(); $denormalizer->denormalize( [], diff --git a/src/Sylius/Bundle/ApiBundle/spec/SerializerContextBuilder/ReadOperationContextBuilderSpec.php b/src/Sylius/Bundle/ApiBundle/spec/SerializerContextBuilder/ReadOperationContextBuilderSpec.php new file mode 100644 index 0000000000..5617133a55 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/SerializerContextBuilder/ReadOperationContextBuilderSpec.php @@ -0,0 +1,83 @@ +beConstructedWith($decoratedSerializerContextBuilder, false, false); + } + + function it_updates_an_context_with_index_and_show_serialization_groups_if_only_read_provided( + Request $request, + SerializerContextBuilderInterface $decoratedSerializerContextBuilder, + ): void { + $decoratedSerializerContextBuilder->createFromRequest($request, true, [])->willReturn([ + 'groups' => ['foo:read'], + ]); + + $this->createFromRequest($request, true, [])->shouldReturn([ + 'groups' => ['foo:read', 'foo:index', 'foo:show'], + ]); + } + + function it_updates_an_context_with_read_serialization_groups_if_only_index_and_show_provided( + Request $request, + SerializerContextBuilderInterface $decoratedSerializerContextBuilder, + ): void { + $decoratedSerializerContextBuilder->createFromRequest($request, true, [])->willReturn([ + 'groups' => ['foo:read'], + ]); + + $this->createFromRequest($request, true, [])->shouldReturn([ + 'groups' => ['foo:read', 'foo:index', 'foo:show'], + ]); + } + + function it_does_not_update_context_with_read_group_if_skip_adding_read_parameter_is_set_to_true( + Request $request, + SerializerContextBuilderInterface $decoratedSerializerContextBuilder, + ): void { + $this->beConstructedWith($decoratedSerializerContextBuilder, true, false); + + $decoratedSerializerContextBuilder->createFromRequest($request, true, [])->willReturn([ + 'groups' => ['foo:show'], + ]); + + $this->createFromRequest($request, true, [])->shouldReturn([ + 'groups' => ['foo:show'], + ]); + } + + function it_does_not_update_context_with_show_and_index_group_if_skip_adding_show_and_index_is_set_to_true( + Request $request, + SerializerContextBuilderInterface $decoratedSerializerContextBuilder, + ): void { + $this->beConstructedWith($decoratedSerializerContextBuilder, false, true); + + $decoratedSerializerContextBuilder->createFromRequest($request, true, [])->willReturn([ + 'groups' => ['foo:read'], + ]); + + $this->createFromRequest($request, true, [])->shouldReturn([ + 'groups' => ['foo:read'], + ]); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Swagger/AcceptLanguageHeaderDocumentationNormalizerSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Swagger/AcceptLanguageHeaderDocumentationNormalizerSpec.php deleted file mode 100644 index 4b5025daad..0000000000 --- a/src/Sylius/Bundle/ApiBundle/spec/Swagger/AcceptLanguageHeaderDocumentationNormalizerSpec.php +++ /dev/null @@ -1,142 +0,0 @@ -beConstructedWith($decoratedNormalizer, $localeRepository); - } - - function it_supports_normalization(NormalizerInterface $decoratedNormalizer): void - { - $documentation = new Documentation(new ResourceNameCollection()); - - $decoratedNormalizer->supportsNormalization($documentation, null)->willReturn(true); - - $this->supportsNormalization($documentation)->shouldReturn(true); - } - - function it_does_not_support_normalization(NormalizerInterface $decoratedNormalizer): void - { - $decoratedNormalizer->supportsNormalization(null, null)->willReturn(false); - - $this->supportsNormalization(null)->shouldReturn(false); - } - - function it_does_not_add_accept_language_header_to_paths_which_are_not_objects( - NormalizerInterface $decoratedNormalizer, - RepositoryInterface $localeRepository, - LocaleInterface $locale1, - LocaleInterface $locale2, - ): void { - $docs = [ - 'paths' => [ - '/api/v2/admin/addresses/{id}' => [ - 'get' => [ - 'parameters' => [], - ], - ], - ], - ]; - - $documentation = new Documentation(new ResourceNameCollection()); - - $localeRepository->findAll()->willReturn([$locale1, $locale2]); - - $locale1->getCode()->willReturn('en_US'); - $locale2->getCode()->willReturn('de_DE'); - - $decoratedNormalizer - ->normalize($documentation, null, ['spec_version' => 3]) - ->willReturn($docs) - ; - - $this - ->normalize($documentation, null, ['spec_version' => 3]) - ->shouldReturn([ - 'paths' => [ - '/api/v2/admin/addresses/{id}' => [ - 'get' => [ - 'parameters' => [], - ], - ], - ], - ]) - ; - } - - function it_adds_accept_language_header_to_paths_which_are_objects( - NormalizerInterface $decoratedNormalizer, - RepositoryInterface $localeRepository, - LocaleInterface $locale1, - LocaleInterface $locale2, - ): void { - $docs = [ - 'paths' => [ - '/api/v2/admin/addresses/{id}' => [ - 'get' => new \ArrayObject( - [ - 'parameters' => [], - ], - ), - ], - ], - ]; - - $documentation = new Documentation(new ResourceNameCollection()); - - $localeRepository->findAll()->willReturn([$locale1, $locale2]); - - $locale1->getCode()->willReturn('en_US'); - $locale2->getCode()->willReturn('de_DE'); - - $decoratedNormalizer - ->normalize($documentation, null, ['spec_version' => 3]) - ->willReturn($docs) - ; - - $this - ->normalize($documentation, null, ['spec_version' => 3]) - ->shouldReturn([ - 'paths' => [ - '/api/v2/admin/addresses/{id}' => [ - 'get' => [ - 'parameters' => [ - [ - 'name' => 'Accept-Language', - 'in' => 'header', - 'required' => false, - 'description' => 'Locales in this enum are all locales defined in the shop and only enabled ones will work in the given channel in the shop.', - 'schema' => [ - 'type' => 'string', - 'enum' => ['en_US', 'de_DE'], - ], - ], - ], - ], - ], - ], - ]) - ; - } -} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/CanPaymentMethodBeChangedValidatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/CanPaymentMethodBeChangedValidatorSpec.php new file mode 100644 index 0000000000..a83b16b71b --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/CanPaymentMethodBeChangedValidatorSpec.php @@ -0,0 +1,111 @@ +beConstructedWith($orderRepository); + + $this->initialize($executionContext); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldImplement(ConstraintValidatorInterface::class); + } + + function it_throws_an_exception_if_value_is_not_change_payment_method_command(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', ['', new CanPaymentMethodBeChanged()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_cannot_change_payment_method_for_cancelled_order( + Constraint $constraint, + ): void { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new ChangePaymentMethod('code'), $constraint]) + ; + } + + function it_throws_an_exception_if_order_is_null(OrderRepositoryInterface $orderRepository): void + { + $command = new ChangePaymentMethod('PAYMENT_METHOD_CODE'); + $command->setOrderTokenValue('ORDER_TOKEN'); + $command->setSubresourceId('123'); + + $orderRepository->findOneByTokenValue('ORDER_TOKEN')->willReturn(null); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [$command, new CanPaymentMethodBeChanged()]) + ; + } + + function it_adds_violation_if_order_is_cancelled( + OrderRepositoryInterface $orderRepository, + OrderInterface $order, + ExecutionContextInterface $executionContext, + ): void { + $command = new ChangePaymentMethod('PAYMENT_METHOD_CODE'); + $command->setOrderTokenValue('ORDER_TOKEN'); + $command->setSubresourceId('123'); + + $orderRepository->findOneByTokenValue('ORDER_TOKEN')->willReturn($order); + $order->getState()->willReturn(OrderInterface::STATE_CANCELLED); + + $executionContext + ->addViolation('sylius.payment_method.cannot_change_payment_method_for_cancelled_order') + ->shouldBeCalled() + ; + + $this->validate($command, new CanPaymentMethodBeChanged()); + } + + function it_does_nothing_if_order_is_not_cancelled( + OrderRepositoryInterface $orderRepository, + OrderInterface $order, + ExecutionContextInterface $executionContext, + ): void { + $command = new ChangePaymentMethod('PAYMENT_METHOD_CODE'); + $command->setOrderTokenValue('ORDER_TOKEN'); + $command->setSubresourceId('123'); + + $orderRepository->findOneByTokenValue('ORDER_TOKEN')->willReturn($order); + $order->getState()->willReturn(OrderInterface::STATE_NEW); + + $executionContext + ->addViolation('sylius.payment_method.cannot_change_payment_method_for_cancelled_order') + ->shouldNotBeCalled() + ; + + $this->validate($command, new CanPaymentMethodBeChanged()); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/CheckoutCompletionValidatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/CheckoutCompletionValidatorSpec.php index 7c55691558..8f8dacb5e3 100644 --- a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/CheckoutCompletionValidatorSpec.php +++ b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/CheckoutCompletionValidatorSpec.php @@ -16,7 +16,8 @@ namespace spec\Sylius\Bundle\ApiBundle\Validator\Constraints; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use SM\Factory\FactoryInterface; -use SM\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\Transition; use Sylius\Bundle\ApiBundle\Command\OrderTokenValueAwareInterface; use Sylius\Bundle\ApiBundle\Validator\Constraints\CheckoutCompletion; use Sylius\Component\Core\Model\OrderInterface; @@ -79,7 +80,7 @@ final class CheckoutCompletionValidatorSpec extends ObjectBehavior ExecutionContextInterface $executionContext, OrderInterface $order, OrderTokenValueAwareInterface $orderTokenValueAware, - StateMachineInterface $stateMachine, + WinzouStateMachineStub $stateMachine, ): void { $this->initialize($executionContext); @@ -105,8 +106,27 @@ final class CheckoutCompletionValidatorSpec extends ObjectBehavior ExecutionContextInterface $executionContext, OrderInterface $order, OrderTokenValueAwareInterface $orderTokenValueAware, - StateMachineInterface $stateMachine, + WinzouStateMachineStub $stateMachine, ): void { + $stateMachine->can('some_possible_transition')->willReturn(true); + $stateMachine->can('another_possible_transition')->willReturn(true); + $stateMachine->can('yet_another_possible_transition')->willReturn(false); + $this->setPropertyValue($stateMachine, 'config', [ + 'transitions' => [ + 'some_possible_transition' => [ + 'from' => [], + 'to' => '', + ], + 'another_possible_transition' => [ + 'from' => [], + 'to' => '', + ], + 'yet_another_possible_transition' => [ + 'from' => [], + 'to' => '', + ], + ], + ]); $this->initialize($executionContext); $orderTokenValueAware->getOrderTokenValue()->willReturn('xxx'); @@ -114,8 +134,8 @@ final class CheckoutCompletionValidatorSpec extends ObjectBehavior $stateMachineFactory->get($order, 'sylius_order_checkout')->willReturn($stateMachine); $stateMachine->can(OrderCheckoutTransitions::TRANSITION_COMPLETE)->willReturn(false); - $stateMachine->getState()->willReturn('some_state_that_does_not_allow_to_complete_order'); - $stateMachine->getPossibleTransitions()->willReturn(['some_possible_transition', 'another_possible_transition']); + + $order->getCheckoutState()->willReturn('some_state_that_does_not_allow_to_complete_order'); $executionContext ->addViolation( @@ -130,4 +150,48 @@ final class CheckoutCompletionValidatorSpec extends ObjectBehavior $this->validate($orderTokenValueAware, new CheckoutCompletion()); } + + function it_uses_the_new_state_machine_abstraction_if_passed( + OrderRepositoryInterface $orderRepository, + StateMachineInterface $stateMachine, + ExecutionContextInterface $executionContext, + OrderInterface $order, + OrderTokenValueAwareInterface $orderTokenValueAware, + ): void { + $this->beConstructedWith($orderRepository, $stateMachine); + + $this->initialize($executionContext); + + $orderTokenValueAware->getOrderTokenValue()->willReturn('xxx'); + $orderRepository->findOneBy(['tokenValue' => 'xxx'])->willReturn($order); + + $stateMachine->can($order, 'sylius_order_checkout', OrderCheckoutTransitions::TRANSITION_COMPLETE)->willReturn(false); + $stateMachine + ->getEnabledTransitions($order, 'sylius_order_checkout') + ->willReturn([new Transition('some_possible_transition', null, null), new Transition('another_possible_transition', null, null)]) + ; + + $order->getCheckoutState()->willReturn('some_state_that_does_not_allow_to_complete_order'); + + $executionContext + ->addViolation( + 'sylius.order.invalid_state_transition', + [ + '%currentState%' => 'some_state_that_does_not_allow_to_complete_order', + '%possibleTransitions%' => 'some_possible_transition, another_possible_transition', + ], + ) + ->shouldBeCalled() + ; + + $this->validate($orderTokenValueAware, new CheckoutCompletion()); + } + + private function setPropertyValue(object $object, string $property, mixed $value): void + { + $reflection = new \ReflectionClass(WinzouStateMachineStub::class); + $reflectionProperty = $reflection->getProperty($property); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($object, $value); + } } diff --git a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenExistsValidatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenExistsValidatorSpec.php new file mode 100644 index 0000000000..fc15e08e74 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenExistsValidatorSpec.php @@ -0,0 +1,83 @@ +beConstructedWith($userRepository); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldImplement(ConstraintValidatorInterface::class); + } + + function it_throws_an_exception_if_value_is_not_a_string(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [null, new ShopUserResetPasswordTokenExists()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_a_reset_password_token_exists_constraint(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', ['', new class() extends Constraint { + }]) + ; + } + + function it_does_not_add_violation_if_user_exists( + UserRepositoryInterface $userRepository, + ExecutionContextInterface $executionContext, + UserInterface $user, + ): void { + $this->initialize($executionContext); + + $userRepository->findOneBy(['passwordResetToken' => 'token'])->willReturn($user); + + $executionContext + ->addViolation('sylius.reset_password.invalid_token', ['%token%' => 'token']) + ->shouldNotBeCalled(); + + $this->validate('token', new ShopUserResetPasswordTokenExists()); + } + + function it_adds_violation_if_reset_password_token_does_not_exist( + UserRepositoryInterface $userRepository, + ExecutionContextInterface $executionContext, + ): void { + $this->initialize($executionContext); + + $userRepository->findOneBy(['passwordResetToken' => 'token'])->willReturn(null); + + $executionContext + ->addViolation('sylius.reset_password.invalid_token', ['%token%' => 'token']) + ->shouldBeCalled(); + + $this->validate('token', new ShopUserResetPasswordTokenExists()); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenNotExpiredValidatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenNotExpiredValidatorSpec.php new file mode 100644 index 0000000000..1b104f7123 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/ShopUserResetPasswordTokenNotExpiredValidatorSpec.php @@ -0,0 +1,108 @@ +beConstructedWith($userRepository, 'P1D'); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldImplement(ConstraintValidatorInterface::class); + } + + function it_throws_an_exception_if_value_is_not_a_string(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [null, new ShopUserResetPasswordTokenNotExpired()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_a_reset_password_token_not_expired_constraint(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', ['', new class() extends Constraint { + }]) + ; + } + + function it_does_not_add_violation_if_reset_password_token_does_not_exist( + UserRepositoryInterface $userRepository, + ExecutionContextInterface $executionContext, + ): void { + $this->initialize($executionContext); + + $userRepository->findOneBy(['passwordResetToken' => 'token'])->willReturn(null); + + $executionContext + ->addViolation('sylius.reset_password.token_expired') + ->shouldNotBeCalled(); + + $this->validate('token', new ShopUserResetPasswordTokenNotExpired()); + } + + function it_does_not_add_violation_if_reset_password_token_is_not_expired( + UserRepositoryInterface $userRepository, + ExecutionContextInterface $executionContext, + UserInterface $user, + ): void { + $this->initialize($executionContext); + + $user->isPasswordRequestNonExpired(Argument::that(function (\DateInterval $dateInterval) { + return $dateInterval->format('%d') === '1'; + }))->willReturn(true); + + $userRepository->findOneBy(['passwordResetToken' => 'token'])->willReturn($user); + + $executionContext + ->addViolation('sylius.reset_password.token_expired') + ->shouldNotBeCalled(); + + $this->validate('token', new ShopUserResetPasswordTokenNotExpired()); + } + + function it_adds_violation_if_reset_password_token_is_expired( + UserRepositoryInterface $userRepository, + ExecutionContextInterface $executionContext, + UserInterface $user, + ): void { + $this->initialize($executionContext); + + $user->isPasswordRequestNonExpired(Argument::that(function (\DateInterval $dateInterval) { + return $dateInterval->format('%d') === '1'; + }))->willReturn(false); + + $userRepository->findOneBy(['passwordResetToken' => 'token'])->willReturn($user); + + $executionContext + ->addViolation('sylius.reset_password.token_expired') + ->shouldBeCalled(); + + $this->validate('token', new ShopUserResetPasswordTokenNotExpired()); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/SingleValueForProductVariantOptionValidatorSpec.php b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/SingleValueForProductVariantOptionValidatorSpec.php new file mode 100644 index 0000000000..122904e1d0 --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/SingleValueForProductVariantOptionValidatorSpec.php @@ -0,0 +1,103 @@ +initialize($executionContext); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldImplement(ConstraintValidatorInterface::class); + } + + function it_throws_an_exception_if_value_is_not_a_product_variant( + ExecutionContextInterface $context, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new \stdClass(), new SingleValueForProductVariantOption()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_a_single_value_for_product_variant_option( + ExecutionContextInterface $context, + ProductVariantInterface $variant, + Constraint $constraint, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [$variant, $constraint]) + ; + } + + function it_adds_violation_if_there_is_more_than_one_option_value_to_a_single_option( + ExecutionContextInterface $executionContext, + ProductVariantInterface $variant, + ProductOptionValueInterface $firstProductOptionValue, + ProductOptionValueInterface $secondProductOptionValue, + ): void { + $constraint = new SingleValueForProductVariantOption(); + + $firstProductOptionValue->getOptionCode()->willReturn('OPTION'); + $secondProductOptionValue->getOptionCode()->willReturn('OPTION'); + + $variant->getOptionValues()->willReturn(new ArrayCollection([ + $firstProductOptionValue->getWrappedObject(), + $secondProductOptionValue->getWrappedObject(), + ])); + + $executionContext->addViolation('sylius.product_variant.option_values.single_value')->shouldBeCalled(); + + $this->validate($variant, $constraint); + } + + function it_does_nothing_if_each_option_has_only_one_option_value( + ExecutionContextInterface $executionContext, + ProductVariantInterface $variant, + ProductOptionValueInterface $firstProductOptionValue, + ProductOptionValueInterface $secondProductOptionValue, + ): void { + $constraint = new SingleValueForProductVariantOption(); + + $firstProductOptionValue->getOptionCode()->willReturn('OPTION'); + $secondProductOptionValue->getOptionCode()->willReturn('DIFFERENT_OPTION'); + + $variant->getOptionValues()->willReturn(new ArrayCollection([ + $firstProductOptionValue->getWrappedObject(), + $secondProductOptionValue->getWrappedObject(), + ])); + + $executionContext->addViolation($constraint->message)->shouldNotBeCalled(); + + $this->validate($variant, $constraint); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/WinzouStateMachineStub.php b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/WinzouStateMachineStub.php new file mode 100644 index 0000000000..fb563d1add --- /dev/null +++ b/src/Sylius/Bundle/ApiBundle/spec/Validator/Constraints/WinzouStateMachineStub.php @@ -0,0 +1,21 @@ +type; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getFormType(): string + { + return $this->formType; + } + + public function getPriority(): int + { + return $this->priority; + } +} diff --git a/src/Sylius/Bundle/AttributeBundle/DependencyInjection/SyliusAttributeExtension.php b/src/Sylius/Bundle/AttributeBundle/DependencyInjection/SyliusAttributeExtension.php index b7cd70c7c4..4c91f85138 100644 --- a/src/Sylius/Bundle/AttributeBundle/DependencyInjection/SyliusAttributeExtension.php +++ b/src/Sylius/Bundle/AttributeBundle/DependencyInjection/SyliusAttributeExtension.php @@ -13,8 +13,10 @@ declare(strict_types=1); namespace Sylius\Bundle\AttributeBundle\DependencyInjection; +use Sylius\Bundle\AttributeBundle\Attribute\AsAttributeType; use Sylius\Bundle\ResourceBundle\DependencyInjection\Extension\AbstractResourceExtension; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -28,6 +30,7 @@ final class SyliusAttributeExtension extends AbstractResourceExtension $loader->load('services.xml'); $this->registerResources('sylius', $config['driver'], $this->resolveResources($config['resources'], $container), $container); + $this->registerAutoconfiguration($container); } private function resolveResources(array $resources, ContainerBuilder $container): array @@ -45,4 +48,19 @@ final class SyliusAttributeExtension extends AbstractResourceExtension return $resolvedResources; } + + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsAttributeType::class, + static function (ChildDefinition $definition, AsAttributeType $attribute): void { + $definition->addTag(AsAttributeType::SERVICE_TAG, [ + 'attribute-type' => $attribute->getType(), + 'label' => $attribute->getLabel(), + 'form-type' => $attribute->getFormType(), + 'priority' => $attribute->getPriority(), + ]); + }, + ); + } } diff --git a/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/Subscriber/LoadMetadataSubscriber.php b/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/Subscriber/LoadMetadataSubscriber.php index 1824b9d908..75bd827521 100644 --- a/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/Subscriber/LoadMetadataSubscriber.php +++ b/src/Sylius/Bundle/AttributeBundle/Doctrine/ORM/Subscriber/LoadMetadataSubscriber.php @@ -50,6 +50,10 @@ final class LoadMetadataSubscriber implements EventSubscriber ClassMetadataInfo $metadata, ClassMetadataFactory $metadataFactory, ): void { + if ($metadata->hasAssociation('subject')) { + return; + } + /** @var ClassMetadataInfo $targetEntityMetadata */ $targetEntityMetadata = $metadataFactory->getMetadataFor($subjectClass); $subjectMapping = [ @@ -64,7 +68,7 @@ final class LoadMetadataSubscriber implements EventSubscriber ]], ]; - $this->mapManyToOne($metadata, $subjectMapping); + $metadata->mapManyToOne($subjectMapping); } private function mapAttributeOnAttributeValue( @@ -72,6 +76,10 @@ final class LoadMetadataSubscriber implements EventSubscriber ClassMetadataInfo $metadata, ClassMetadataFactory $metadataFactory, ): void { + if ($metadata->hasAssociation('attribute')) { + return; + } + /** @var ClassMetadataInfo $attributeMetadata */ $attributeMetadata = $metadataFactory->getMetadataFor($attributeClass); $attributeMapping = [ @@ -84,11 +92,6 @@ final class LoadMetadataSubscriber implements EventSubscriber ]], ]; - $this->mapManyToOne($metadata, $attributeMapping); - } - - private function mapManyToOne(ClassMetadataInfo $metadata, array $subjectMapping): void - { - $metadata->mapManyToOne($subjectMapping); + $metadata->mapManyToOne($attributeMapping); } } diff --git a/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeType/Configuration/SelectAttributeChoicesCollectionType.php b/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeType/Configuration/SelectAttributeChoicesCollectionType.php index aeea8672e7..d9b7ea1a10 100644 --- a/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeType/Configuration/SelectAttributeChoicesCollectionType.php +++ b/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeType/Configuration/SelectAttributeChoicesCollectionType.php @@ -28,6 +28,14 @@ class SelectAttributeChoicesCollectionType extends AbstractType public function __construct(?TranslationLocaleProviderInterface $localeProvider = null) { if (null !== $localeProvider) { + trigger_deprecation( + 'sylius/attribute-bundle', + '1.13', + 'Passing an instance of %s as a constructor argument for %s is deprecated and will not be possible in Sylius 2.0.', + TranslationLocaleProviderInterface::class, + self::class, + ); + $this->defaultLocaleCode = $localeProvider->getDefaultLocaleCode(); } } diff --git a/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeType/FloatAttributeType.php b/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeType/FloatAttributeType.php new file mode 100644 index 0000000000..88369dce77 --- /dev/null +++ b/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeType/FloatAttributeType.php @@ -0,0 +1,43 @@ +setDefaults([ + 'label' => false, + 'html5' => true, + ]) + ->setRequired('configuration') + ->setDefined('locale_code') + ; + } + + public function getBlockPrefix(): string + { + return 'sylius_attribute_type_float'; + } +} diff --git a/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeValueType.php b/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeValueType.php index 20b4542edd..8addbeaf9a 100644 --- a/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeValueType.php +++ b/src/Sylius/Bundle/AttributeBundle/Form/Type/AttributeValueType.php @@ -13,13 +13,16 @@ declare(strict_types=1); namespace Sylius\Bundle\AttributeBundle\Form\Type; +use Sylius\Bundle\LocaleBundle\Form\DataTransformer\LocaleToCodeTransformer; use Sylius\Bundle\LocaleBundle\Form\Type\LocaleChoiceType; use Sylius\Bundle\ResourceBundle\Form\DataTransformer\ResourceToIdentifierTransformer; use Sylius\Bundle\ResourceBundle\Form\Registry\FormTypeRegistryInterface; use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; use Sylius\Component\Attribute\Model\AttributeInterface; use Sylius\Component\Attribute\Model\AttributeValueInterface; +use Sylius\Component\Locale\Model\LocaleInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; +use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; @@ -28,6 +31,9 @@ use Symfony\Component\Form\ReversedTransformer; abstract class AttributeValueType extends AbstractResourceType { + /** + * @param DataTransformerInterface|null $localeToCodeTransformer + */ public function __construct( string $dataClass, array $validationGroups, @@ -35,7 +41,18 @@ abstract class AttributeValueType extends AbstractResourceType protected RepositoryInterface $attributeRepository, protected RepositoryInterface $localeRepository, protected FormTypeRegistryInterface $formTypeRegistry, + protected ?DataTransformerInterface $localeToCodeTransformer = null, ) { + if (null === $this->localeToCodeTransformer) { + trigger_deprecation( + 'sylius/attribute-bundle', + '1.13', + 'Not passing a "%s" instance as argument 7 to "%s" is deprecated. It will not be possible in Sylius 2.0.', + LocaleToCodeTransformer::class, + self::class, + ); + } + parent::__construct($dataClass, $validationGroups); } @@ -78,7 +95,7 @@ abstract class AttributeValueType extends AbstractResourceType ; $builder->get('localeCode')->addModelTransformer( - new ReversedTransformer(new ResourceToIdentifierTransformer($this->localeRepository, 'code')), + new ReversedTransformer($this->localeToCodeTransformer ?? new ResourceToIdentifierTransformer($this->localeRepository, 'code')), ); } diff --git a/src/Sylius/Bundle/AttributeBundle/Resources/config/services.xml b/src/Sylius/Bundle/AttributeBundle/Resources/config/services.xml index d9944604b2..55bb48adfa 100644 --- a/src/Sylius/Bundle/AttributeBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/AttributeBundle/Resources/config/services.xml @@ -40,6 +40,11 @@ + + + + + diff --git a/src/Sylius/Bundle/AttributeBundle/Resources/config/services/attribute_types.xml b/src/Sylius/Bundle/AttributeBundle/Resources/config/services/attribute_types.xml index ff08a924ae..002b625ea0 100644 --- a/src/Sylius/Bundle/AttributeBundle/Resources/config/services/attribute_types.xml +++ b/src/Sylius/Bundle/AttributeBundle/Resources/config/services/attribute_types.xml @@ -36,6 +36,11 @@ form-type="Sylius\Bundle\AttributeBundle\Form\Type\AttributeType\IntegerAttributeType" /> + + + + diff --git a/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/Attribute.xml b/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/Attribute.xml index 1d51098a7f..0d679137e2 100644 --- a/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/Attribute.xml +++ b/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/Attribute.xml @@ -24,6 +24,9 @@ + + + diff --git a/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/AttributeTranslation.xml b/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/AttributeTranslation.xml index 6587628f32..2deff9a5a2 100644 --- a/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/AttributeTranslation.xml +++ b/src/Sylius/Bundle/AttributeBundle/Resources/config/validation/AttributeTranslation.xml @@ -13,6 +13,14 @@ + + + + + @@ -26,5 +34,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/AttributeBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/AttributeBundle/Resources/translations/messages.en.yml index 505f2f20c5..28b9a3473e 100644 --- a/src/Sylius/Bundle/AttributeBundle/Resources/translations/messages.en.yml +++ b/src/Sylius/Bundle/AttributeBundle/Resources/translations/messages.en.yml @@ -14,6 +14,7 @@ sylius: configuration: Configuration date: Date datetime: Datetime + float: Float integer: Integer percent: Percent select: Select diff --git a/src/Sylius/Bundle/AttributeBundle/Resources/translations/messages.fr.yml b/src/Sylius/Bundle/AttributeBundle/Resources/translations/messages.fr.yml index 023e27302e..220f9fb9bd 100644 --- a/src/Sylius/Bundle/AttributeBundle/Resources/translations/messages.fr.yml +++ b/src/Sylius/Bundle/AttributeBundle/Resources/translations/messages.fr.yml @@ -17,6 +17,7 @@ sylius: configuration: Configuration date: Date datetime: Date et heure + float: Décimal integer: Entier percent: Pourcentage select: Sélectionner diff --git a/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.en.yml index 1ef0a32a0a..fe5438ccdc 100644 --- a/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.en.yml @@ -17,8 +17,15 @@ sylius: not_blank: Please enter attribute presentation. min_length: Attribute presentation must be at least 1 character long.|Attribute presentation must be at least {{ limit }} characters long. max_length: Attribute presentation must not be longer than 1 character.|Attribute presentation must not be longer than {{ limit }} characters. + type: + unregistered: The attribute type "%type%" is not registered. attribute_value: attribute: not_blank: Please select attribute. value: not_blank: Please enter attribute value. + translation: + locale: + not_blank: Please enter the locale. + invalid: This value is not a valid locale. + unique: A translation for the {{ value }} locale code already exists. diff --git a/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.fr.yml b/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.fr.yml index 07b53472c0..d4f4c42777 100644 --- a/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.fr.yml +++ b/src/Sylius/Bundle/AttributeBundle/Resources/translations/validators.fr.yml @@ -20,8 +20,15 @@ sylius: not_blank: Veuillez entrer une description de l'attribut. min_length: La description de l'attribut doit contenir au moins 1 caractère.|La description de l'attribut doit contenir au moins {{ limit }} caractères. max_length: La description de l'attribut ne doit pas contenir plus de 1 caractère.|La description de l'attribut ne doit pas contenir plus de {{ limit }} caractères. + type: + unregistered: Le type d'attribut "%type%" n'est pas enregistré. attribute_value: attribute: not_blank: Veuillez sélectionner un attribut. value: not_blank: Veuillez entrer une valeur d'attribut. + translation: + locale: + not_blank: Vous devez entrer une valeur. + invalid: Ce paramètre régional n'est pas valide. + unique: Une traduction pour la locale {{ value }} existe déjà. diff --git a/src/Sylius/Bundle/AttributeBundle/Tests/DependencyInjection/SyliusAttributeExtensionTest.php b/src/Sylius/Bundle/AttributeBundle/Tests/DependencyInjection/SyliusAttributeExtensionTest.php new file mode 100644 index 0000000000..e95f1cd959 --- /dev/null +++ b/src/Sylius/Bundle/AttributeBundle/Tests/DependencyInjection/SyliusAttributeExtensionTest.php @@ -0,0 +1,53 @@ +container->setDefinition( + 'acme.attribute_type_with_attribute', + (new Definition()) + ->setClass(AttributeTypeStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.attribute_type_with_attribute', + AsAttributeType::SERVICE_TAG, + [ + 'attribute-type' => 'test', + 'label' => 'Test', + 'form-type' => 'SomeFormType', + 'priority' => 15, + ], + ); + } + + protected function getContainerExtensions(): array + { + return [new SyliusAttributeExtension()]; + } +} diff --git a/src/Sylius/Bundle/AttributeBundle/Tests/Form/Type/AttributeType/SelectAttributeTypeTest.php b/src/Sylius/Bundle/AttributeBundle/Tests/Form/Type/AttributeType/SelectAttributeTypeTest.php index 4857325229..8ac8bed1c2 100644 --- a/src/Sylius/Bundle/AttributeBundle/Tests/Form/Type/AttributeType/SelectAttributeTypeTest.php +++ b/src/Sylius/Bundle/AttributeBundle/Tests/Form/Type/AttributeType/SelectAttributeTypeTest.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Bundle\AttributeBundle\Tests\Form\Type\AttributeType; use PHPUnit\Framework\Assert; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Sylius\Bundle\AttributeBundle\Form\Type\AttributeType\SelectAttributeType; use Sylius\Component\Resource\Translation\Provider\TranslationLocaleProviderInterface; @@ -23,6 +24,8 @@ use Symfony\Component\Form\Test\TypeTestCase; final class SelectAttributeTypeTest extends TypeTestCase { + use ProphecyTrait; + private ObjectProphecy $translationProvider; /** diff --git a/src/Sylius/Bundle/AttributeBundle/Tests/Stub/AttributeTypeStub.php b/src/Sylius/Bundle/AttributeBundle/Tests/Stub/AttributeTypeStub.php new file mode 100644 index 0000000000..c43ed96bbd --- /dev/null +++ b/src/Sylius/Bundle/AttributeBundle/Tests/Stub/AttributeTypeStub.php @@ -0,0 +1,37 @@ +getType(); + if (null === $type || $this->attributeTypeRegistry->has($type)) { + return; + } + + $this->context + ->buildViolation($constraint->unregisteredAttributeTypeMessage, ['%type%' => $type]) + ->atPath('type') + ->addViolation() + ; + } +} diff --git a/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidAttributeValueValidator.php b/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidAttributeValueValidator.php index 159f9ebe28..096702ff86 100644 --- a/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidAttributeValueValidator.php +++ b/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidAttributeValueValidator.php @@ -26,7 +26,7 @@ final class ValidAttributeValueValidator extends ConstraintValidator { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (!$value instanceof AttributeValueInterface) { throw new UnexpectedTypeException($value::class, AttributeValueInterface::class); diff --git a/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidSelectAttributeConfigurationValidator.php b/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidSelectAttributeConfigurationValidator.php index 86593f47ba..d6ba4d4262 100644 --- a/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidSelectAttributeConfigurationValidator.php +++ b/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidSelectAttributeConfigurationValidator.php @@ -21,7 +21,7 @@ use Webmozart\Assert\Assert; final class ValidSelectAttributeConfigurationValidator extends ConstraintValidator { - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var AttributeInterface $value */ Assert::isInstanceOf($value, AttributeInterface::class); @@ -49,7 +49,7 @@ final class ValidSelectAttributeConfigurationValidator extends ConstraintValidat return; } - $multiple = $value->getConfiguration()['multiple']; + $multiple = $value->getConfiguration()['multiple'] ?? false; if (!$multiple) { $this->context->addViolation($constraint->messageMultiple); diff --git a/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidTextAttributeConfigurationValidator.php b/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidTextAttributeConfigurationValidator.php index a8f9613b01..17cc5d3c04 100644 --- a/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidTextAttributeConfigurationValidator.php +++ b/src/Sylius/Bundle/AttributeBundle/Validator/Constraints/ValidTextAttributeConfigurationValidator.php @@ -21,7 +21,7 @@ use Webmozart\Assert\Assert; final class ValidTextAttributeConfigurationValidator extends ConstraintValidator { - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var AttributeInterface $value */ Assert::isInstanceOf($value, AttributeInterface::class); diff --git a/src/Sylius/Bundle/AttributeBundle/composer.json b/src/Sylius/Bundle/AttributeBundle/composer.json index b23f0dfa94..537c428053 100644 --- a/src/Sylius/Bundle/AttributeBundle/composer.json +++ b/src/Sylius/Bundle/AttributeBundle/composer.json @@ -26,20 +26,22 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "ramsey/uuid": "^4.0", "stof/doctrine-extensions-bundle": "^1.4", - "sylius/attribute": "^1.12", + "sylius/attribute": "^1.13", "sylius/resource-bundle": "^1.9", - "symfony/framework-bundle": "^5.4 || ^6.0" + "symfony/framework-bundle": "^5.4.21 || ^6.4" }, "require-dev": { "doctrine/orm": "^2.13", + "matthiasnoback/symfony-dependency-injection-test": "^4.2", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0" + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4" }, "conflict": { "doctrine/orm": ">= 2.16.0", @@ -52,7 +54,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { diff --git a/src/Sylius/Bundle/AttributeBundle/spec/Doctrine/ORM/Subscriber/LoadMetadataSubscriberSpec.php b/src/Sylius/Bundle/AttributeBundle/spec/Doctrine/ORM/Subscriber/LoadMetadataSubscriberSpec.php index 0fe9514151..1cef5923e8 100644 --- a/src/Sylius/Bundle/AttributeBundle/spec/Doctrine/ORM/Subscriber/LoadMetadataSubscriberSpec.php +++ b/src/Sylius/Bundle/AttributeBundle/spec/Doctrine/ORM/Subscriber/LoadMetadataSubscriberSpec.php @@ -58,23 +58,6 @@ final class LoadMetadataSubscriberSpec extends ObjectBehavior $this->getSubscribedEvents()->shouldReturn(['loadClassMetadata']); } - function it_does_not_add_a_many_to_one_mapping_if_the_class_is_not_a_configured_attribute_value_model( - LoadClassMetadataEventArgs $eventArgs, - ClassMetadataInfo $metadata, - EntityManager $entityManager, - ClassMetadataFactory $classMetadataFactory, - ): void { - $eventArgs->getEntityManager()->willReturn($entityManager); - $entityManager->getMetadataFactory()->willReturn($classMetadataFactory); - - $eventArgs->getClassMetadata()->willReturn($metadata); - $metadata->getName()->willReturn('KeepMoving\ThisClass\DoesNot\Concern\You'); - - $metadata->mapManyToOne(Argument::any())->shouldNotBeCalled(); - - $this->loadClassMetadata($eventArgs); - } - function it_maps_many_to_one_associations_from_the_attribute_value_model_to_the_subject_model_and_the_attribute_model( LoadClassMetadataEventArgs $eventArgs, ClassMetadataInfo $metadata, @@ -82,6 +65,7 @@ final class LoadMetadataSubscriberSpec extends ObjectBehavior ClassMetadataFactory $classMetadataFactory, ClassMetadataInfo $classMetadataInfo, ): void { + $eventArgs->getClassMetadata()->willReturn($metadata); $eventArgs->getEntityManager()->willReturn($entityManager); $entityManager->getMetadataFactory()->willReturn($classMetadataFactory); $classMetadataInfo->fieldMappings = [ @@ -92,9 +76,9 @@ final class LoadMetadataSubscriberSpec extends ObjectBehavior $classMetadataFactory->getMetadataFor('Some\App\Product\Entity\Product')->willReturn($classMetadataInfo); $classMetadataFactory->getMetadataFor('Some\App\Product\Entity\Attribute')->willReturn($classMetadataInfo); - $eventArgs->getClassMetadata()->willReturn($metadata); - $eventArgs->getClassMetadata()->willReturn($metadata); $metadata->getName()->willReturn('Some\App\Product\Entity\AttributeValue'); + $metadata->hasAssociation('subject')->willReturn(false); + $metadata->hasAssociation('attribute')->willReturn(false); $subjectMapping = [ 'fieldName' => 'subject', @@ -124,6 +108,42 @@ final class LoadMetadataSubscriberSpec extends ObjectBehavior $this->loadClassMetadata($eventArgs); } + function it_does_not_map_relations_for_attribute_value_model_if_the_relations_already_exist( + LoadClassMetadataEventArgs $eventArgs, + ClassMetadataInfo $metadata, + EntityManager $entityManager, + ClassMetadataFactory $classMetadataFactory, + ): void { + $eventArgs->getClassMetadata()->willReturn($metadata); + $eventArgs->getEntityManager()->willReturn($entityManager); + $entityManager->getMetadataFactory()->willReturn($classMetadataFactory); + + $metadata->getName()->willReturn('Some\App\Product\Entity\AttributeValue'); + $metadata->hasAssociation('subject')->willReturn(true); + $metadata->hasAssociation('attribute')->willReturn(true); + + $metadata->mapManyToOne(Argument::any())->shouldNotBeCalled(); + + $this->loadClassMetadata($eventArgs); + } + + function it_does_not_add_a_many_to_one_mapping_if_the_class_is_not_a_configured_attribute_value_model( + LoadClassMetadataEventArgs $eventArgs, + ClassMetadataInfo $metadata, + EntityManager $entityManager, + ClassMetadataFactory $classMetadataFactory, + ): void { + $eventArgs->getEntityManager()->willReturn($entityManager); + $entityManager->getMetadataFactory()->willReturn($classMetadataFactory); + + $eventArgs->getClassMetadata()->willReturn($metadata); + $metadata->getName()->willReturn('KeepMoving\ThisClass\DoesNot\Concern\You'); + + $metadata->mapManyToOne(Argument::any())->shouldNotBeCalled(); + + $this->loadClassMetadata($eventArgs); + } + function it_does_not_add_values_one_to_many_mapping_if_the_class_is_not_a_configured_attribute_model( LoadClassMetadataEventArgs $eventArgs, ClassMetadataInfo $metadata, diff --git a/src/Sylius/Bundle/AttributeBundle/spec/Validator/Constraints/AttributeTypeValidatorSpec.php b/src/Sylius/Bundle/AttributeBundle/spec/Validator/Constraints/AttributeTypeValidatorSpec.php new file mode 100644 index 0000000000..cbd2af70fa --- /dev/null +++ b/src/Sylius/Bundle/AttributeBundle/spec/Validator/Constraints/AttributeTypeValidatorSpec.php @@ -0,0 +1,105 @@ +beConstructedWith($attributeTypesRegistry); + $this->initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldHaveType(ConstraintValidator::class); + } + + function it_throws_exception_when_value_is_not_an_attribute(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new \stdClass(), new AttributeType()]) + ; + } + + function it_throws_exception_when_constraint_is_not_an_attribute_type( + AttributeInterface $attribute, + Constraint $constraint, + ): void { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [$attribute, $constraint]) + ; + } + + function it_does_nothing_when_attribute_type_is_null( + ServiceRegistryInterface $attributeTypesRegistry, + ExecutionContextInterface $context, + AttributeInterface $attribute, + ): void { + $attribute->getType()->willReturn(null); + + $attributeTypesRegistry->has(Argument::any())->shouldNotBeCalled(); + $context->addViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate($attribute, new AttributeType()); + } + + function it_does_nothing_when_attribute_type_is_registered( + ServiceRegistryInterface $attributeTypesRegistry, + ExecutionContextInterface $context, + AttributeInterface $attribute, + ): void { + $attribute->getType()->willReturn('foo'); + + $attributeTypesRegistry->has('foo')->willReturn(true); + + $context->addViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate($attribute, new AttributeType()); + } + + function it_adds_violation_when_attribute_type_is_not_registered( + ServiceRegistryInterface $attributeTypesRegistry, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $violationBuilder, + AttributeInterface $attribute, + ): void { + $constraint = new AttributeType(); + + $attribute->getType()->willReturn('foo'); + $attributeTypesRegistry->has('foo')->willReturn(false); + + $context + ->buildViolation($constraint->unregisteredAttributeTypeMessage, ['%type%' => 'foo']) + ->shouldBeCalled() + ->willReturn($violationBuilder) + ; + $violationBuilder->atPath('type')->shouldBeCalled()->willReturn($violationBuilder); + $violationBuilder->addViolation()->shouldBeCalled(); + + $this->validate($attribute, $constraint); + } +} diff --git a/src/Sylius/Bundle/AttributeBundle/spec/Validator/Constraints/ValidSelectAttributeConfigurationValidatorSpec.php b/src/Sylius/Bundle/AttributeBundle/spec/Validator/Constraints/ValidSelectAttributeConfigurationValidatorSpec.php index dd643bf88c..e744a46d01 100644 --- a/src/Sylius/Bundle/AttributeBundle/spec/Validator/Constraints/ValidSelectAttributeConfigurationValidatorSpec.php +++ b/src/Sylius/Bundle/AttributeBundle/spec/Validator/Constraints/ValidSelectAttributeConfigurationValidatorSpec.php @@ -85,6 +85,20 @@ final class ValidSelectAttributeConfigurationValidatorSpec extends ObjectBehavio $this->validate($attribute, $constraint); } + function it_adds_a_violation_if_multiple_is_not_set_when_min_or_max_entries_values_are_specified( + ExecutionContextInterface $context, + AttributeInterface $attribute, + ): void { + $constraint = new ValidSelectAttributeConfiguration(); + + $attribute->getType()->willReturn(SelectAttributeType::TYPE); + $attribute->getConfiguration()->willReturn(['min' => 4, 'max' => 6]); + + $context->addViolation(Argument::any())->shouldBeCalled(); + + $this->validate($attribute, $constraint); + } + function it_does_nothing_if_an_attribute_is_not_a_select_type( ExecutionContextInterface $context, AttributeInterface $attribute, diff --git a/src/Sylius/Bundle/ChannelBundle/Attribute/AsChannelContext.php b/src/Sylius/Bundle/ChannelBundle/Attribute/AsChannelContext.php new file mode 100644 index 0000000000..13abdf3adb --- /dev/null +++ b/src/Sylius/Bundle/ChannelBundle/Attribute/AsChannelContext.php @@ -0,0 +1,29 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/ChannelBundle/Attribute/AsRequestBasedChannelResolver.php b/src/Sylius/Bundle/ChannelBundle/Attribute/AsRequestBasedChannelResolver.php new file mode 100644 index 0000000000..4780d984af --- /dev/null +++ b/src/Sylius/Bundle/ChannelBundle/Attribute/AsRequestBasedChannelResolver.php @@ -0,0 +1,29 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/ChannelBundle/Collector/ChannelCollector.php b/src/Sylius/Bundle/ChannelBundle/Collector/ChannelCollector.php index 18a7de172f..a705da5d9a 100644 --- a/src/Sylius/Bundle/ChannelBundle/Collector/ChannelCollector.php +++ b/src/Sylius/Bundle/ChannelBundle/Collector/ChannelCollector.php @@ -58,7 +58,7 @@ final class ChannelCollector extends DataCollector $this->data = [ 'channel' => $channel, - 'channels' => array_map([$this, 'pluckChannel'], $this->channelRepository->findAll()), + 'channels' => $this->channelRepository->findAllWithBasicData(), 'channel_change_support' => $this->channelChangeSupport, ]; } diff --git a/src/Sylius/Bundle/ChannelBundle/DependencyInjection/SyliusChannelExtension.php b/src/Sylius/Bundle/ChannelBundle/DependencyInjection/SyliusChannelExtension.php index 22482a05f2..d5c6f99df6 100644 --- a/src/Sylius/Bundle/ChannelBundle/DependencyInjection/SyliusChannelExtension.php +++ b/src/Sylius/Bundle/ChannelBundle/DependencyInjection/SyliusChannelExtension.php @@ -13,8 +13,11 @@ declare(strict_types=1); namespace Sylius\Bundle\ChannelBundle\DependencyInjection; +use Sylius\Bundle\ChannelBundle\Attribute\AsChannelContext; +use Sylius\Bundle\ChannelBundle\Attribute\AsRequestBasedChannelResolver; use Sylius\Bundle\ResourceBundle\DependencyInjection\Extension\AbstractResourceExtension; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -38,5 +41,24 @@ final class SyliusChannelExtension extends AbstractResourceExtension } $container->getDefinition('sylius.repository.channel')->setLazy(true); + + $this->registerAutoconfiguration($container); + } + + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsChannelContext::class, + static function (ChildDefinition $definition, AsChannelContext $attribute): void { + $definition->addTag(AsChannelContext::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsRequestBasedChannelResolver::class, + static function (ChildDefinition $definition, AsRequestBasedChannelResolver $attribute): void { + $definition->addTag(AsRequestBasedChannelResolver::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); } } diff --git a/src/Sylius/Bundle/ChannelBundle/Doctrine/ORM/ChannelRepository.php b/src/Sylius/Bundle/ChannelBundle/Doctrine/ORM/ChannelRepository.php index 2e56372d0b..6b27728119 100644 --- a/src/Sylius/Bundle/ChannelBundle/Doctrine/ORM/ChannelRepository.php +++ b/src/Sylius/Bundle/ChannelBundle/Doctrine/ORM/ChannelRepository.php @@ -13,10 +13,16 @@ declare(strict_types=1); namespace Sylius\Bundle\ChannelBundle\Doctrine\ORM; +use Doctrine\ORM\AbstractQuery; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Channel\Model\ChannelInterface; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; +/** + * @template T of ChannelInterface + * + * @implements ChannelRepositoryInterface + */ class ChannelRepository extends EntityRepository implements ChannelRepositoryInterface { private const ORDER_BY = ['id' => 'ASC']; @@ -40,4 +46,29 @@ class ChannelRepository extends EntityRepository implements ChannelRepositoryInt { return $this->findBy(['name' => $name], self::ORDER_BY); } + + public function findAllWithBasicData(): iterable + { + return $this->createQueryBuilder('o') + ->select(['o.code', 'o.name', 'o.hostname']) + ->getQuery() + ->getResult(AbstractQuery::HYDRATE_ARRAY) + ; + } + + /** + * @return ChannelInterface[] + */ + public function findEnabled(): iterable + { + /** @var ChannelInterface[] $enabledChannels */ + $enabledChannels = $this->findBy(['enabled' => true]); + + return $enabledChannels; + } + + public function countAll(): int + { + return $this->count([]); + } } diff --git a/src/Sylius/Bundle/ChannelBundle/Resources/config/services.xml b/src/Sylius/Bundle/ChannelBundle/Resources/config/services.xml index 6a6c3f3c52..cc0fdf70d8 100644 --- a/src/Sylius/Bundle/ChannelBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/ChannelBundle/Resources/config/services.xml @@ -13,6 +13,10 @@ + + sylius + + sylius @@ -65,5 +69,10 @@ false + + + + + diff --git a/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/Compiler/CompositeChannelContextPassTest.php b/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/Compiler/CompositeChannelContextPassTest.php index 0a29f3b821..7dc0d97cf2 100644 --- a/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/Compiler/CompositeChannelContextPassTest.php +++ b/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/Compiler/CompositeChannelContextPassTest.php @@ -21,7 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class CompositeChannelContextPassTest extends AbstractCompilerPassTestCase +final class CompositeChannelContextPassTest extends AbstractCompilerPassTestCase { /** * @test diff --git a/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/Compiler/CompositeRequestResolverPassTest.php b/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/Compiler/CompositeRequestResolverPassTest.php index 44530c01bd..63f89b327a 100644 --- a/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/Compiler/CompositeRequestResolverPassTest.php +++ b/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/Compiler/CompositeRequestResolverPassTest.php @@ -21,7 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -class CompositeRequestResolverPassTest extends AbstractCompilerPassTestCase +final class CompositeRequestResolverPassTest extends AbstractCompilerPassTestCase { /** * @test diff --git a/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/SyliusChannelExtensionTest.php b/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/SyliusChannelExtensionTest.php index c698052b29..fd988ad251 100644 --- a/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/SyliusChannelExtensionTest.php +++ b/src/Sylius/Bundle/ChannelBundle/Tests/DependencyInjection/SyliusChannelExtensionTest.php @@ -14,7 +14,12 @@ declare(strict_types=1); namespace Sylius\Bundle\ChannelBundle\Tests\DependencyInjection; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; +use Sylius\Bundle\ChannelBundle\Attribute\AsChannelContext; +use Sylius\Bundle\ChannelBundle\Attribute\AsRequestBasedChannelResolver; use Sylius\Bundle\ChannelBundle\DependencyInjection\SyliusChannelExtension; +use Sylius\Bundle\ChannelBundle\Tests\Stub\ChannelContextStub; +use Sylius\Bundle\ChannelBundle\Tests\Stub\RequestBestChannelResolverStub; +use Symfony\Component\DependencyInjection\Definition; final class SyliusChannelExtensionTest extends AbstractExtensionTestCase { @@ -54,6 +59,46 @@ final class SyliusChannelExtensionTest extends AbstractExtensionTestCase $this->assertContainerBuilderHasServiceDefinitionWithArgument('sylius.channel_collector', 2, false); } + /** @test */ + public function it_autoconfigures_channel_context_with_attribute(): void + { + $this->container->setDefinition( + 'acme.channel_context_with_attribute', + (new Definition()) + ->setClass(ChannelContextStub::class) + ->setAutoconfigured(true), + ); + + $this->load(['debug' => false]); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.channel_context_with_attribute', + AsChannelContext::SERVICE_TAG, + ['priority' => 15], + ); + } + + /** @test */ + public function it_autoconfigures_request_based_channel_resolver_with_attribute(): void + { + $this->container->setDefinition( + 'acme.channel_context_request_resolver_with_attribute', + (new Definition()) + ->setClass(RequestBestChannelResolverStub::class) + ->setAutoconfigured(true), + ); + + $this->load(['debug' => false]); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.channel_context_request_resolver_with_attribute', + AsRequestBasedChannelResolver::SERVICE_TAG, + ['priority' => 20], + ); + } + protected function getContainerExtensions(): array { return [ diff --git a/src/Sylius/Bundle/ChannelBundle/Tests/Stub/ChannelContextStub.php b/src/Sylius/Bundle/ChannelBundle/Tests/Stub/ChannelContextStub.php new file mode 100644 index 0000000000..523f9b636f --- /dev/null +++ b/src/Sylius/Bundle/ChannelBundle/Tests/Stub/ChannelContextStub.php @@ -0,0 +1,28 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Attribute/AsCatalogPromotionPriceCalculator.php b/src/Sylius/Bundle/CoreBundle/Attribute/AsCatalogPromotionPriceCalculator.php new file mode 100644 index 0000000000..fa8a30a32e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Attribute/AsCatalogPromotionPriceCalculator.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Attribute/AsEntityObserver.php b/src/Sylius/Bundle/CoreBundle/Attribute/AsEntityObserver.php new file mode 100644 index 0000000000..b7c6f87cc9 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Attribute/AsEntityObserver.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Attribute/AsOrderItemUnitsTaxesApplicator.php b/src/Sylius/Bundle/CoreBundle/Attribute/AsOrderItemUnitsTaxesApplicator.php new file mode 100644 index 0000000000..c8a605899d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Attribute/AsOrderItemUnitsTaxesApplicator.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Attribute/AsOrderItemsTaxesApplicator.php b/src/Sylius/Bundle/CoreBundle/Attribute/AsOrderItemsTaxesApplicator.php new file mode 100644 index 0000000000..d91c5f3092 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Attribute/AsOrderItemsTaxesApplicator.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Attribute/AsOrdersTotalsProvider.php b/src/Sylius/Bundle/CoreBundle/Attribute/AsOrdersTotalsProvider.php new file mode 100644 index 0000000000..455a26c8df --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Attribute/AsOrdersTotalsProvider.php @@ -0,0 +1,33 @@ +type; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Attribute/AsProductVariantMapProvider.php b/src/Sylius/Bundle/CoreBundle/Attribute/AsProductVariantMapProvider.php new file mode 100644 index 0000000000..c4b9c7d557 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Attribute/AsProductVariantMapProvider.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Attribute/AsTaxCalculationStrategy.php b/src/Sylius/Bundle/CoreBundle/Attribute/AsTaxCalculationStrategy.php new file mode 100644 index 0000000000..fc5f466932 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Attribute/AsTaxCalculationStrategy.php @@ -0,0 +1,42 @@ +type; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getPriority(): int + { + return $this->priority; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Attribute/AsUriBasedSectionResolver.php b/src/Sylius/Bundle/CoreBundle/Attribute/AsUriBasedSectionResolver.php new file mode 100644 index 0000000000..290b549943 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Attribute/AsUriBasedSectionResolver.php @@ -0,0 +1,29 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Announcer/CatalogPromotionRemovalAnnouncer.php b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Announcer/CatalogPromotionRemovalAnnouncer.php new file mode 100644 index 0000000000..b65c9de519 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Announcer/CatalogPromotionRemovalAnnouncer.php @@ -0,0 +1,38 @@ +commandBus->dispatch(new UpdateCatalogPromotionState($catalogPromotion->getCode())); + + if ($catalogPromotion->isEnabled()) { + $this->commandBus->dispatch(new DisableCatalogPromotion($catalogPromotion->getCode())); + } + + $this->commandBus->dispatch(new RemoveCatalogPromotion($catalogPromotion->getCode())); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Announcer/CatalogPromotionRemovalAnnouncerInterface.php b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Announcer/CatalogPromotionRemovalAnnouncerInterface.php new file mode 100644 index 0000000000..e7dc6a4cae --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Announcer/CatalogPromotionRemovalAnnouncerInterface.php @@ -0,0 +1,21 @@ +getChannels() as $channel) { $channelPricing = $variant->getChannelPricingForChannel($channel); if ($channelPricing === null) { diff --git a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Command/DisableCatalogPromotion.php b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Command/DisableCatalogPromotion.php new file mode 100644 index 0000000000..d8b224460f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Command/DisableCatalogPromotion.php @@ -0,0 +1,21 @@ +catalogPromotionRepository->findOneBy(['code' => $command->code]); + + if (null === $catalogPromotion) { + return; + } + + $catalogPromotion->disable(); + $this->allProductVariantsCatalogPromotionsProcessor->process(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/RemoveCatalogPromotionHandler.php b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/RemoveCatalogPromotionHandler.php new file mode 100644 index 0000000000..7da29850fc --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/RemoveCatalogPromotionHandler.php @@ -0,0 +1,51 @@ +catalogPromotionRepository->findOneBy(['code' => $command->code]); + + if (null === $catalogPromotion) { + return; + } + + if ($catalogPromotion->getState() !== CatalogPromotionStates::STATE_PROCESSING) { + throw new InvalidCatalogPromotionStateException( + sprintf( + 'Catalog promotion with code "%s" cannot be removed as it is not in a processing state.', + $catalogPromotion->getCode(), + ), + ); + } + + $this->entityManager->remove($catalogPromotion); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/RemoveInactiveCatalogPromotionHandler.php b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/RemoveInactiveCatalogPromotionHandler.php index 50affeb20b..2fd5a15c40 100644 --- a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/RemoveInactiveCatalogPromotionHandler.php +++ b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/CommandHandler/RemoveInactiveCatalogPromotionHandler.php @@ -20,12 +20,20 @@ use Sylius\Component\Promotion\Exception\InvalidCatalogPromotionStateException; use Sylius\Component\Promotion\Model\CatalogPromotionStates; use Sylius\Component\Promotion\Repository\CatalogPromotionRepositoryInterface; +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see RemoveCatalogPromotionHandler} instead. */ final class RemoveInactiveCatalogPromotionHandler { public function __construct( private CatalogPromotionRepositoryInterface $catalogPromotionRepository, private EntityManager $entityManager, ) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + self::class, + RemoveCatalogPromotionHandler::class, + ); } public function __invoke(RemoveInactiveCatalogPromotion $command): void diff --git a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Listener/CatalogPromotionStateChangedListener.php b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Listener/CatalogPromotionStateChangedListener.php index 8f3ccd5f25..e0d5799259 100644 --- a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Listener/CatalogPromotionStateChangedListener.php +++ b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Listener/CatalogPromotionStateChangedListener.php @@ -25,7 +25,7 @@ final class CatalogPromotionStateChangedListener { } - public function __invoke(CatalogPromotionCreated|CatalogPromotionUpdated|CatalogPromotionEnded $event): void + public function __invoke(CatalogPromotionCreated|CatalogPromotionEnded|CatalogPromotionUpdated $event): void { $this->messageBus->dispatch(new UpdateCatalogPromotionState($event->code)); } diff --git a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/CatalogPromotionRemovalProcessor.php b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/CatalogPromotionRemovalProcessor.php index 0d1d981d83..5da2786ab0 100644 --- a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/CatalogPromotionRemovalProcessor.php +++ b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/CatalogPromotionRemovalProcessor.php @@ -13,9 +13,9 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\CatalogPromotion\Processor; -use Sylius\Bundle\CoreBundle\CatalogPromotion\Command\RemoveInactiveCatalogPromotion; +use Sylius\Bundle\CoreBundle\CatalogPromotion\Announcer\CatalogPromotionRemovalAnnouncer; +use Sylius\Bundle\CoreBundle\CatalogPromotion\Announcer\CatalogPromotionRemovalAnnouncerInterface; use Sylius\Component\Core\Model\CatalogPromotionInterface; -use Sylius\Component\Promotion\Event\CatalogPromotionEnded; use Sylius\Component\Promotion\Exception\CatalogPromotionNotFoundException; use Sylius\Component\Promotion\Exception\InvalidCatalogPromotionStateException; use Sylius\Component\Promotion\Model\CatalogPromotionStates; @@ -26,9 +26,31 @@ final class CatalogPromotionRemovalProcessor implements CatalogPromotionRemovalP { public function __construct( private CatalogPromotionRepositoryInterface $catalogPromotionRepository, - private MessageBusInterface $commandBus, - private MessageBusInterface $eventBus, + /** @var CatalogPromotionRemovalAnnouncerInterface $catalogPromotionRemovalAnnouncer */ + private CatalogPromotionRemovalAnnouncerInterface|MessageBusInterface $catalogPromotionRemovalAnnouncer, + private ?MessageBusInterface $eventBus = null, /** @phpstan-ignore-line */ ) { + if ($catalogPromotionRemovalAnnouncer instanceof MessageBusInterface) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'Passing an instance of %s as second constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + MessageBusInterface::class, + self::class, + CatalogPromotionRemovalAnnouncerInterface::class, + ); + + $this->catalogPromotionRemovalAnnouncer = new CatalogPromotionRemovalAnnouncer($catalogPromotionRemovalAnnouncer); + } + + if (null !== $eventBus) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'Passing third constructor argument for %s is deprecated and will be removed in Sylius 2.0.', + self::class, + ); + } } public function removeCatalogPromotion(string $catalogPromotionCode): void @@ -36,20 +58,6 @@ final class CatalogPromotionRemovalProcessor implements CatalogPromotionRemovalP /** @var CatalogPromotionInterface|null $catalogPromotion */ $catalogPromotion = $this->getCatalogPromotion($catalogPromotionCode); - if ($catalogPromotion->getState() === CatalogPromotionStates::STATE_INACTIVE) { - $this->announceInactiveCatalogPromotionRemoval($catalogPromotionCode); - - return; - } - - if ($catalogPromotion->getState() === CatalogPromotionStates::STATE_ACTIVE) { - $this->disableCatalogPromotion($catalogPromotion); - $this->announceCatalogPromotionEnd($catalogPromotionCode); - $this->announceInactiveCatalogPromotionRemoval($catalogPromotionCode); - - return; - } - if ($catalogPromotion->getState() === CatalogPromotionStates::STATE_PROCESSING) { throw new InvalidCatalogPromotionStateException( sprintf( @@ -59,22 +67,11 @@ final class CatalogPromotionRemovalProcessor implements CatalogPromotionRemovalP ); } - throw new \DomainException('Invalid catalog promotion state.'); - } + if (!in_array($catalogPromotion->getState(), [CatalogPromotionStates::STATE_ACTIVE, CatalogPromotionStates::STATE_INACTIVE], true)) { + throw new \DomainException('Invalid catalog promotion state.'); + } - private function announceCatalogPromotionEnd(string $catalogPromotionCode): void - { - $this->eventBus->dispatch(new CatalogPromotionEnded($catalogPromotionCode)); - } - - private function announceInactiveCatalogPromotionRemoval(string $catalogPromotionCode): void - { - $this->commandBus->dispatch(new RemoveInactiveCatalogPromotion($catalogPromotionCode)); - } - - private function disableCatalogPromotion(CatalogPromotionInterface $catalogPromotion): void - { - $catalogPromotion->setEnabled(false); + $this->catalogPromotionRemovalAnnouncer->dispatchCatalogPromotionRemoval($catalogPromotion); } private function getCatalogPromotion(string $catalogPromotionCode): CatalogPromotionInterface diff --git a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/CatalogPromotionStateProcessor.php b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/CatalogPromotionStateProcessor.php index ca4272516f..4791329fad 100644 --- a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/CatalogPromotionStateProcessor.php +++ b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/CatalogPromotionStateProcessor.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\CatalogPromotion\Processor; use SM\Factory\FactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Bundle\CoreBundle\CatalogPromotion\Checker\CatalogPromotionEligibilityCheckerInterface; use Sylius\Component\Core\Model\CatalogPromotionInterface; use Sylius\Component\Promotion\Model\CatalogPromotionTransitions; @@ -22,30 +24,50 @@ final class CatalogPromotionStateProcessor implements CatalogPromotionStateProce { public function __construct( private CatalogPromotionEligibilityCheckerInterface $catalogPromotionEligibilityChecker, - private FactoryInterface $stateMachineFactory, + private FactoryInterface|StateMachineInterface $stateMachineFactory, ) { + if ($this->stateMachineFactory instanceof FactoryInterface) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the second argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + FactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function process(CatalogPromotionInterface $catalogPromotion): void { - $stateMachine = $this->stateMachineFactory->get($catalogPromotion, CatalogPromotionTransitions::GRAPH); + $stateMachine = $this->getStateMachine(); - if ($stateMachine->can(CatalogPromotionTransitions::TRANSITION_PROCESS)) { - $stateMachine->apply(CatalogPromotionTransitions::TRANSITION_PROCESS); + if ($stateMachine->can($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_PROCESS)) { + $stateMachine->apply($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_PROCESS); return; } if (!$this->catalogPromotionEligibilityChecker->isCatalogPromotionEligible($catalogPromotion)) { - if ($stateMachine->can(CatalogPromotionTransitions::TRANSITION_DEACTIVATE)) { - $stateMachine->apply(CatalogPromotionTransitions::TRANSITION_DEACTIVATE); + if ($stateMachine->can($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_DEACTIVATE)) { + $stateMachine->apply($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_DEACTIVATE); } return; } - if ($stateMachine->can(CatalogPromotionTransitions::TRANSITION_ACTIVATE)) { - $stateMachine->apply(CatalogPromotionTransitions::TRANSITION_ACTIVATE); + if ($stateMachine->can($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_ACTIVATE)) { + $stateMachine->apply($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_ACTIVATE); } } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof FactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } } diff --git a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/ProductCatalogPromotionsProcessor.php b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/ProductCatalogPromotionsProcessor.php index 288645c07b..a553b7b4ec 100644 --- a/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/ProductCatalogPromotionsProcessor.php +++ b/src/Sylius/Bundle/CoreBundle/CatalogPromotion/Processor/ProductCatalogPromotionsProcessor.php @@ -15,7 +15,7 @@ namespace Sylius\Bundle\CoreBundle\CatalogPromotion\Processor; use Sylius\Bundle\CoreBundle\CatalogPromotion\CommandDispatcher\ApplyCatalogPromotionsOnVariantsCommandDispatcherInterface; use Sylius\Component\Core\Model\ProductInterface; -use Sylius\Component\Core\Model\ProductVariantInterface; +use Sylius\Component\Product\Model\ProductVariantInterface; final class ProductCatalogPromotionsProcessor implements ProductCatalogPromotionsProcessorInterface { diff --git a/src/Sylius/Bundle/CoreBundle/Checkout/CheckoutResolver.php b/src/Sylius/Bundle/CoreBundle/Checkout/CheckoutResolver.php index 6b8f3ba2b3..62498cb9bf 100644 --- a/src/Sylius/Bundle/CoreBundle/Checkout/CheckoutResolver.php +++ b/src/Sylius/Bundle/CoreBundle/Checkout/CheckoutResolver.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Checkout; use SM\Factory\FactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Order\Context\CartContextInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -29,8 +31,19 @@ final class CheckoutResolver implements EventSubscriberInterface private CartContextInterface $cartContext, private CheckoutStateUrlGeneratorInterface $urlGenerator, private RequestMatcherInterface $requestMatcher, - private FactoryInterface $stateMachineFactory, + private FactoryInterface|StateMachineInterface $stateMachineFactory, ) { + if ($this->stateMachineFactory instanceof FactoryInterface) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the fourth argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + FactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public static function getSubscribedEvents(): array @@ -73,9 +86,7 @@ final class CheckoutResolver implements EventSubscriberInterface return; } - $stateMachine = $this->stateMachineFactory->get($order, $graph); - - if ($stateMachine->can($transition)) { + if ($this->getStateMachine()->can($order, $graph, $transition)) { return; } @@ -91,4 +102,13 @@ final class CheckoutResolver implements EventSubscriberInterface { return $request->attributes->get('_sylius', [])['state_machine']['transition'] ?? null; } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof FactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php b/src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php index f53034d2f3..cdf06593a8 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/AbstractInstallCommand.php @@ -13,114 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use Doctrine\ORM\EntityManagerInterface; -use Sylius\Bundle\CoreBundle\Installer\Executor\CommandExecutor; -use SyliusLabs\Polyfill\Symfony\FrameworkBundle\Command\ContainerAwareCommand; -use Symfony\Component\Console\Helper\ProgressBar; -use Symfony\Component\Console\Helper\Table; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\NullOutput; -use Symfony\Component\Console\Output\OutputInterface; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + AbstractInstallCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\AbstractInstallCommand::class, +); -abstract class AbstractInstallCommand extends ContainerAwareCommand -{ - /** @deprecated */ - public const WEB_ASSETS_DIRECTORY = 'web/assets/'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\AbstractInstallCommand::class); - /** @deprecated */ - public const WEB_BUNDLES_DIRECTORY = 'web/bundles/'; - - /** @deprecated */ - public const WEB_MEDIA_DIRECTORY = 'web/media/'; - - /** @deprecated */ - public const WEB_MEDIA_IMAGE_DIRECTORY = 'web/media/image/'; - - /** @var CommandExecutor|null */ - protected $commandExecutor; - - protected function initialize(InputInterface $input, OutputInterface $output) +if (false) { + abstract class AbstractInstallCommand { - $application = $this->getApplication(); - $application->setCatchExceptions(false); - - $this->commandExecutor = new CommandExecutor($input, $output, $application); - } - - /** - * @return object - */ - protected function get(string $id) - { - return $this->getContainer()->get($id); - } - - protected function getEnvironment(): string - { - return (string) $this->getContainer()->getParameter('kernel.environment'); - } - - protected function isDebug(): bool - { - return (bool) $this->getContainer()->getParameter('kernel.debug'); - } - - protected function renderTable(array $headers, array $rows, OutputInterface $output): void - { - $table = new Table($output); - - $table - ->setHeaders($headers) - ->setRows($rows) - ->render() - ; - } - - protected function createProgressBar(OutputInterface $output, int $length = 10): ProgressBar - { - $progress = new ProgressBar($output); - $progress->setBarCharacter(''); - $progress->setEmptyBarCharacter(' '); - $progress->setProgressCharacter(''); - - $progress->start($length); - - return $progress; - } - - protected function runCommands(array $commands, OutputInterface $output, bool $displayProgress = true): void - { - $progress = $this->createProgressBar($displayProgress ? $output : new NullOutput(), count($commands)); - - foreach ($commands as $key => $value) { - if (is_string($key)) { - $command = $key; - $parameters = $value; - } else { - $command = $value; - $parameters = []; - } - - $this->commandExecutor->runCommand($command, $parameters); - - // PDO does not always close the connection after Doctrine commands. - // See https://github.com/symfony/symfony/issues/11750. - /** @var EntityManagerInterface $entityManager */ - $entityManager = $this->getContainer()->get('doctrine')->getManager(); - $entityManager->getConnection()->close(); - - $progress->advance(); - } - - $progress->finish(); - } - - protected function ensureDirectoryExistsAndIsWritable(string $directory, OutputInterface $output): void - { - $checker = $this->getContainer()->get('sylius.installer.checker.command_directory'); - $checker->setCommandName($this->getName()); - - $checker->ensureDirectoryExists($directory, $output); - $checker->ensureDirectoryIsWritable($directory, $output); } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/CancelUnpaidOrdersCommand.php b/src/Sylius/Bundle/CoreBundle/Command/CancelUnpaidOrdersCommand.php index 1762994ee4..fe6852f336 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/CancelUnpaidOrdersCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/CancelUnpaidOrdersCommand.php @@ -13,41 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use SyliusLabs\Polyfill\Symfony\FrameworkBundle\Command\ContainerAwareCommand; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + CancelUnpaidOrdersCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\CancelUnpaidOrdersCommand::class, +); -/** - * @final - */ -class CancelUnpaidOrdersCommand extends ContainerAwareCommand -{ - protected static $defaultName = 'sylius:cancel-unpaid-orders'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\CancelUnpaidOrdersCommand::class); - protected function configure(): void +if (false) { + final class CancelUnpaidOrdersCommand { - $this - ->setDescription( - 'Removes order that have been unpaid for a configured period. Configuration parameter - sylius_order.order_expiration_period.', - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $expirationTime = $this->getContainer()->getParameter('sylius_order.order_expiration_period'); - $output->writeln(sprintf( - 'Command will cancel orders that have been unpaid for %s.', - (string) $expirationTime, - )); - - $unpaidCartsStateUpdater = $this->getContainer()->get('sylius.unpaid_orders_state_updater'); - $unpaidCartsStateUpdater->cancel(); - - $this->getContainer()->get('sylius.manager.order')->flush(); - - $output->writeln('Unpaid orders have been canceled'); - - return 0; } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/CheckRequirementsCommand.php b/src/Sylius/Bundle/CoreBundle/Command/CheckRequirementsCommand.php index df49cb0d88..0699e42c4c 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/CheckRequirementsCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/CheckRequirementsCommand.php @@ -13,38 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use RuntimeException; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + CheckRequirementsCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\CheckRequirementsCommand::class, +); -final class CheckRequirementsCommand extends AbstractInstallCommand -{ - protected static $defaultName = 'sylius:install:check-requirements'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\CheckRequirementsCommand::class); - protected function configure(): void +if (false) { + final class CheckRequirementsCommand { - $this - ->setDescription('Checks if all Sylius requirements are satisfied.') - ->setHelp( - <<%command.name% command checks system requirements. -EOT - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $fulfilled = $this->getContainer()->get('sylius.installer.checker.sylius_requirements')->check($input, $output); - - if (!$fulfilled) { - throw new RuntimeException( - 'Some system requirements are not fulfilled. Please check output messages and fix them.', - ); - } - - $output->writeln('Success! Your system can run Sylius properly.'); - - return 0; } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/InformAboutGUSCommand.php b/src/Sylius/Bundle/CoreBundle/Command/InformAboutGUSCommand.php index 0c914ad58e..12f8b6b565 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/InformAboutGUSCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/InformAboutGUSCommand.php @@ -13,39 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + InformAboutGUSCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\InformAboutGUSCommand::class, +); -final class InformAboutGUSCommand extends Command -{ - protected static $defaultName = 'sylius:inform-about-gus'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\InformAboutGUSCommand::class); - protected function configure(): void +if (false) { + final class InformAboutGUSCommand { - $this->setDescription('Informs about Sylius internal statistical service'); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $style = new SymfonyStyle($input, $output); - - $style->note( - [ - 'For purely statistical purposes and in order to inform you about important updates and security patches, Sylius might send non-sensitive data to our servers. We send:', - '* Hostname', - '* User-agent', - '* Locale', - '* Environment (test, dev or prod)', - '* Currently used Sylius version', - '* Date of the last contact', - 'If you do not consent please follow this cookbook article:', - 'https://docs.sylius.com/en/latest/cookbook/configuration/disabling-admin-notifications.html', - 'That being said, every time we get a notification about a new site deployed with Sylius, it brings a huge smile to our face and motivates us to continue our Open Source work.', - ], - ); - - return 0; } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/InstallAssetsCommand.php b/src/Sylius/Bundle/CoreBundle/Command/InstallAssetsCommand.php index 00090b9208..21b14c12e7 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/InstallAssetsCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/InstallAssetsCommand.php @@ -13,49 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + InstallAssetsCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\InstallAssetsCommand::class, +); -final class InstallAssetsCommand extends AbstractInstallCommand -{ - protected static $defaultName = 'sylius:install:assets'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\InstallAssetsCommand::class); - protected function configure(): void +if (false) { + final class InstallAssetsCommand { - $this - ->setDescription('Installs all Sylius assets.') - ->setHelp( - <<%command.name% command downloads and installs all Sylius media assets. -EOT - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $output->writeln(sprintf( - 'Installing Sylius assets for environment %s.', - $this->getEnvironment(), - )); - - try { - $publicDir = $this->getContainer()->getParameter('sylius_core.public_dir'); - - $this->ensureDirectoryExistsAndIsWritable($publicDir . '/assets/', $output); - $this->ensureDirectoryExistsAndIsWritable($publicDir . '/bundles/', $output); - } catch (\RuntimeException $exception) { - $output->writeln($exception->getMessage()); - - return 1; - } - - $commands = [ - 'assets:install' => ['target' => $publicDir], - ]; - - $this->runCommands($commands, $output); - - return 0; } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/InstallCommand.php b/src/Sylius/Bundle/CoreBundle/Command/InstallCommand.php index 6039c5e39e..9945f326e5 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/InstallCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/InstallCommand.php @@ -13,124 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Process\Exception\RuntimeException; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + InstallCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\InstallCommand::class, +); -final class InstallCommand extends AbstractInstallCommand -{ - protected static $defaultName = 'sylius:install'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\InstallCommand::class); - private array $commands = [ - [ - 'command' => 'check-requirements', - 'message' => 'Checking system requirements.', - ], - [ - 'command' => 'database', - 'message' => 'Setting up the database.', - ], - [ - 'command' => 'setup', - 'message' => 'Shop configuration.', - ], - [ - 'command' => 'jwt-setup', - 'message' => 'Configuring JWT token.', - ], - [ - 'command' => 'assets', - 'message' => 'Installing assets.', - ], - ]; - - protected function configure(): void +if (false) { + final class InstallCommand { - $this - ->setDescription('Installs Sylius in your preferred environment.') - ->setHelp( - <<%command.name% command installs Sylius. -EOT - ) - ->addOption('fixture-suite', 's', InputOption::VALUE_OPTIONAL, 'Load specified fixture suite during install', null) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $suite = $input->getOption('fixture-suite'); - - $outputStyle = new SymfonyStyle($input, $output); - $outputStyle->writeln('Installing Sylius...'); - $outputStyle->writeln($this->getSyliusLogo()); - - $this->ensureDirectoryExistsAndIsWritable((string) $this->getContainer()->getParameter('kernel.cache_dir'), $output); - - $errored = false; - /** - * @var int $step - * @var array $command - */ - foreach ($this->commands as $step => $command) { - try { - $outputStyle->newLine(); - $outputStyle->section(sprintf( - 'Step %d of %d. %s', - $step + 1, - count($this->commands), - $command['message'], - )); - - $parameters = []; - if ('database' === $command['command'] && null !== $suite) { - $parameters['--fixture-suite'] = $suite; - } - - $this->commandExecutor->runCommand('sylius:install:' . $command['command'], $parameters, $output); - } catch (RuntimeException) { - $errored = true; - } - } - - $outputStyle->newLine(2); - $outputStyle->success($this->getProperFinalMessage($errored)); - $outputStyle->writeln('You can now open your store at the following path under the website root: /'); - - return $errored ? 1 : 0; - } - - private function getProperFinalMessage(bool $errored): string - { - if ($errored) { - return 'Sylius has been installed, but some error occurred.'; - } - - return 'Sylius has been successfully installed.'; - } - - private function getSyliusLogo(): string - { - return ' - , - ,;:, - `;;;.:` - `::;` :` - :::` ` .\'++: \'\'. \'. - `::: :+\',;+\' :+; `+. - :::: +\' :\' `+; - `:::, \'+` ++ :+.`+; `++. ;+\' \'\' ,++++. - ,:::` `++\'. .+: `+\' `+; .+, ;+ +\' +; \'\' - ::::` ,+++. \'+` :+. `+; `+, ;+ +\' \'+. - ,. .:::: .++` `+: +\' `+; `+, ;+ +\' `;++; -`;;.:::` ::::: :+. \'+,+. `+; `+, ;+ `+\' .++ - .;;;;;;::`.::::, +\'` `++ `++\' `+; `+: :+. `++\' \'. ;+ - ,;;;;;;;;;::::: .+++++` ;+, ++; ++, `\'+++,\'+\' :++++, - ,;;;;;;;;;:::` ;\' - :;;;;;;;;;:, :.:+, - ;;;;;;;;;: ;++,' - ; } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/InstallDatabaseCommand.php b/src/Sylius/Bundle/CoreBundle/Command/InstallDatabaseCommand.php index f34d655c7c..9838eeeebc 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/InstallDatabaseCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/InstallDatabaseCommand.php @@ -13,53 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + InstallDatabaseCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\InstallDatabaseCommand::class, +); -final class InstallDatabaseCommand extends AbstractInstallCommand -{ - protected static $defaultName = 'sylius:install:database'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\InstallDatabaseCommand::class); - protected function configure(): void +if (false) { + final class InstallDatabaseCommand { - $this - ->setDescription('Install Sylius database.') - ->setHelp( - <<%command.name% command creates Sylius database. -EOT - ) - ->addOption('fixture-suite', 's', InputOption::VALUE_OPTIONAL, 'Load specified fixture suite during install', null) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $suite = $input->getOption('fixture-suite'); - - $outputStyle = new SymfonyStyle($input, $output); - $outputStyle->writeln(sprintf( - 'Creating Sylius database for environment %s.', - $this->getEnvironment(), - )); - - $commands = $this - ->getContainer() - ->get('sylius.commands_provider.database_setup') - ->getCommands($input, $output, $this->getHelper('question')) - ; - - $this->runCommands($commands, $output); - $outputStyle->newLine(); - - $parameters = []; - if (null !== $suite) { - $parameters['--fixture-suite'] = $suite; - } - $this->commandExecutor->runCommand('sylius:install:sample-data', $parameters, $output); - - return 0; } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/InstallSampleDataCommand.php b/src/Sylius/Bundle/CoreBundle/Command/InstallSampleDataCommand.php index 80b873098d..6f66c36217 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/InstallSampleDataCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/InstallSampleDataCommand.php @@ -13,75 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\ConfirmationQuestion; -use Symfony\Component\Console\Style\SymfonyStyle; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + InstallSampleDataCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\InstallSampleDataCommand::class, +); -final class InstallSampleDataCommand extends AbstractInstallCommand -{ - protected static $defaultName = 'sylius:install:sample-data'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\InstallSampleDataCommand::class); - protected function configure(): void +if (false) { + final class InstallSampleDataCommand { - $this - ->setDescription('Install sample data into Sylius.') - ->setHelp( - <<%command.name% command loads the sample data for Sylius. -EOT - ) - ->addOption('fixture-suite', 's', InputOption::VALUE_OPTIONAL, 'Load specified fixture suite during install', null) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - /** @var QuestionHelper $questionHelper */ - $questionHelper = $this->getHelper('question'); - $suite = $input->getOption('fixture-suite'); - - $outputStyle = new SymfonyStyle($input, $output); - $outputStyle->newLine(); - $outputStyle->writeln(sprintf( - 'Loading sample data for environment %s from suite %s.', - $this->getEnvironment(), - $suite ?? 'default', - )); - $outputStyle->writeln('Warning! This action will erase your database.'); - - if (!$questionHelper->ask($input, $output, new ConfirmationQuestion('Continue? (y/N) ', null !== $suite))) { - $outputStyle->writeln('Cancelled loading sample data.'); - - return 0; - } - - try { - $publicDir = $this->getContainer()->getParameter('sylius_core.public_dir'); - - $this->ensureDirectoryExistsAndIsWritable($publicDir . '/media/', $output); - $this->ensureDirectoryExistsAndIsWritable($publicDir . '/media/image/', $output); - } catch (\RuntimeException $exception) { - $outputStyle->writeln($exception->getMessage()); - - return 1; - } - - $parameters = ['--no-interaction' => true]; - - if (null !== $suite) { - $parameters['suite'] = $suite; - } - - $commands = [ - 'sylius:fixtures:load' => $parameters, - ]; - - $this->runCommands($commands, $output); - $outputStyle->newLine(2); - - return 0; } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/JwtConfigurationCommand.php b/src/Sylius/Bundle/CoreBundle/Command/JwtConfigurationCommand.php index 4844550fd6..e45fa7a512 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/JwtConfigurationCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/JwtConfigurationCommand.php @@ -13,48 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use Symfony\Component\Console\Helper\HelperInterface; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\ConfirmationQuestion; -use Webmozart\Assert\Assert; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + JwtConfigurationCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\JwtConfigurationCommand::class, +); -final class JwtConfigurationCommand extends AbstractInstallCommand -{ - protected static $defaultName = 'sylius:install:jwt-setup'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\JwtConfigurationCommand::class); - protected function configure(): void +if (false) { + final class JwtConfigurationCommand { - $this - ->setDescription('Setup JWT token') - ->setHelp( - <<%command.name% command generates JWT token. -EOT - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - /** @var HelperInterface $helper */ - $helper = $this->getHelper('question'); - - $output->writeln('Generating JWT token for Sylius API'); - - $question = new ConfirmationQuestion('Do you want to generate JWT token? (y/N)', false); - - Assert::isInstanceOf($helper, QuestionHelper::class); - if (!$helper->ask($input, $output, $question)) { - return 0; - } - - $this->commandExecutor->runCommand('lexik:jwt:generate-keypair', ['--overwrite' => true], $output); - - $output->writeln('Please, remember to enable Sylius API'); - $output->writeln('https://docs.sylius.com/en/1.10/book/api/introduction.html'); - - return 0; } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/Model/PluginInfo.php b/src/Sylius/Bundle/CoreBundle/Command/Model/PluginInfo.php index 3db8425f11..7ea2179355 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/Model/PluginInfo.php +++ b/src/Sylius/Bundle/CoreBundle/Command/Model/PluginInfo.php @@ -13,24 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command\Model; -final class PluginInfo -{ - public function __construct(private string $name, private string $description, private string $url) - { - } +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + PluginInfo::class, + \Sylius\Bundle\CoreBundle\Console\Command\Model\PluginInfo::class, +); - public function name(): string - { - return $this->name; - } +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\Model\PluginInfo::class); - public function description(): string +if (false) { + final class PluginInfo { - return $this->description; - } - - public function url(): string - { - return $this->url; } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/SetupCommand.php b/src/Sylius/Bundle/CoreBundle/Command/SetupCommand.php index 2c5184bca7..efb1c962ac 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/SetupCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/SetupCommand.php @@ -13,204 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use Sylius\Component\Core\Model\AdminUserInterface; -use Sylius\Component\User\Repository\UserRepositoryInterface; -use Symfony\Component\Console\Helper\QuestionHelper; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Question\Question; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Validator\Constraints\Email; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\ConstraintViolationListInterface; -use Webmozart\Assert\Assert; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + SetupCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\SetupCommand::class, +); -final class SetupCommand extends AbstractInstallCommand -{ - protected static $defaultName = 'sylius:install:setup'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\SetupCommand::class); - protected function configure(): void +if (false) { + final class SetupCommand { - $this - ->setDescription('Sylius configuration setup.') - ->setHelp( - <<%command.name% command allows user to configure basic Sylius data. -EOT - ) - ; - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $currency = $this->getContainer()->get('sylius.setup.currency')->setup($input, $output, $this->getHelper('question')); - $locale = $this->getContainer()->get('sylius.setup.locale')->setup($input, $output, $this->getHelper('question')); - $this->getContainer()->get('sylius.setup.channel')->setup($locale, $currency); - $this->setupAdministratorUser($input, $output, $locale->getCode()); - - return 0; - } - - protected function setupAdministratorUser(InputInterface $input, OutputInterface $output, string $localeCode): void - { - $outputStyle = new SymfonyStyle($input, $output); - $outputStyle->writeln('Create your administrator account.'); - - $userManager = $this->getContainer()->get('sylius.manager.admin_user'); - $userFactory = $this->getContainer()->get('sylius.factory.admin_user'); - - try { - $user = $this->configureNewUser($userFactory->createNew(), $input, $output); - } catch (\InvalidArgumentException) { - return; - } - - $user->setEnabled(true); - $user->setLocaleCode($localeCode); - - $userManager->persist($user); - $userManager->flush(); - - $outputStyle->writeln('Administrator account successfully registered.'); - $outputStyle->newLine(); - } - - private function configureNewUser( - AdminUserInterface $user, - InputInterface $input, - OutputInterface $output, - ): AdminUserInterface { - /** @var UserRepositoryInterface $userRepository */ - $userRepository = $this->getAdminUserRepository(); - - if ($input->getOption('no-interaction')) { - Assert::null($userRepository->findOneByEmail('sylius@example.com')); - - $user->setEmail('sylius@example.com'); - $user->setUsername('sylius'); - $user->setPlainPassword('sylius'); - - return $user; - } - - $email = $this->getAdministratorEmail($input, $output); - $user->setEmail($email); - $user->setUsername($this->getAdministratorUsername($input, $output, $email)); - $user->setPlainPassword($this->getAdministratorPassword($input, $output)); - - return $user; - } - - private function createEmailQuestion(): Question - { - return (new Question('E-mail: ')) - ->setValidator( - /** - * @param mixed $value - */ - function ($value): string { - /** @var ConstraintViolationListInterface $errors */ - $errors = $this->getContainer()->get('validator')->validate((string) $value, [new Email(), new NotBlank()]); - foreach ($errors as $error) { - throw new \DomainException((string) $error->getMessage()); - } - - return $value; - }, - ) - ->setMaxAttempts(3) - ; - } - - private function getAdministratorEmail(InputInterface $input, OutputInterface $output): string - { - /** @var QuestionHelper $questionHelper */ - $questionHelper = $this->getHelper('question'); - /** @var UserRepositoryInterface $userRepository */ - $userRepository = $this->getAdminUserRepository(); - - do { - $question = $this->createEmailQuestion(); - $email = $questionHelper->ask($input, $output, $question); - $exists = null !== $userRepository->findOneByEmail($email); - - if ($exists) { - $output->writeln('E-Mail is already in use!'); - } - } while ($exists); - - return $email; - } - - private function getAdministratorUsername(InputInterface $input, OutputInterface $output, string $email): string - { - /** @var QuestionHelper $questionHelper */ - $questionHelper = $this->getHelper('question'); - /** @var UserRepositoryInterface $userRepository */ - $userRepository = $this->getAdminUserRepository(); - - do { - $question = new Question('Username (press enter to use email): ', $email); - $username = $questionHelper->ask($input, $output, $question); - $exists = null !== $userRepository->findOneBy(['username' => $username]); - - if ($exists) { - $output->writeln('Username is already in use!'); - } - } while ($exists); - - return $username; - } - - private function getAdministratorPassword(InputInterface $input, OutputInterface $output): string - { - /** @var QuestionHelper $questionHelper */ - $questionHelper = $this->getHelper('question'); - $validator = $this->getPasswordQuestionValidator(); - - do { - $passwordQuestion = $this->createPasswordQuestion('Choose password:', $validator); - $confirmPasswordQuestion = $this->createPasswordQuestion('Confirm password:', $validator); - - $password = $questionHelper->ask($input, $output, $passwordQuestion); - $repeatedPassword = $questionHelper->ask($input, $output, $confirmPasswordQuestion); - - if ($repeatedPassword !== $password) { - $output->writeln('Passwords do not match!'); - } - } while ($repeatedPassword !== $password); - - return $password; - } - - private function getPasswordQuestionValidator(): \Closure - { - return - /** @param mixed $value */ - function ($value): string { - /** @var ConstraintViolationListInterface $errors */ - $errors = $this->getContainer()->get('validator')->validate($value, [new NotBlank()]); - foreach ($errors as $error) { - throw new \DomainException((string) $error->getMessage()); - } - - return $value; - } - ; - } - - private function createPasswordQuestion(string $message, \Closure $validator): Question - { - return (new Question($message)) - ->setValidator($validator) - ->setMaxAttempts(3) - ->setHidden(true) - ->setHiddenFallback(false) - ; - } - - private function getAdminUserRepository(): UserRepositoryInterface - { - return $this->getContainer()->get('sylius.repository.admin_user'); } } diff --git a/src/Sylius/Bundle/CoreBundle/Command/ShowAvailablePluginsCommand.php b/src/Sylius/Bundle/CoreBundle/Command/ShowAvailablePluginsCommand.php index 1db789f579..0e64f3362f 100644 --- a/src/Sylius/Bundle/CoreBundle/Command/ShowAvailablePluginsCommand.php +++ b/src/Sylius/Bundle/CoreBundle/Command/ShowAvailablePluginsCommand.php @@ -13,52 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Command; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; -use Sylius\Bundle\CoreBundle\Command\Model\PluginInfo; -use Sylius\Bundle\CoreBundle\Installer\Renderer\TableRenderer; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + ShowAvailablePluginsCommand::class, + \Sylius\Bundle\CoreBundle\Console\Command\ShowAvailablePluginsCommand::class, +); -final class ShowAvailablePluginsCommand extends Command -{ - protected static $defaultName = 'sylius:show-available-plugins'; +class_exists(\Sylius\Bundle\CoreBundle\Console\Command\ShowAvailablePluginsCommand::class); - /** @var ArrayCollection */ - private Collection $plugins; - - protected function configure(): void +if (false) { + final class ShowAvailablePluginsCommand { - $this->setDescription('Shows official Sylius Plugins'); - $this->configurePlugins(); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $output->writeln('Available official plugins and selected community plugins:'); - - $pluginTable = new TableRenderer($output); - $pluginTable->setHeaders(['Plugin', 'Description', 'URL']); - - foreach ($this->plugins as $plugin) { - $pluginTable->addRow([sprintf('%s', $plugin->name()), $plugin->description(), $plugin->url()]); - } - - $pluginTable->render(); - - return 0; - } - - private function configurePlugins(): void - { - $this->plugins = new ArrayCollection(); - - $this->plugins->add(new PluginInfo('Admin Order Creation', 'Creating (and copying) orders in the administration panel.', 'https://github.com/Sylius/AdminOrderCreationPlugin')); - $this->plugins->add(new PluginInfo('Customer Order Cancellation', 'Allows customers to quickly cancel their unpaid and unshipped orders.', 'https://github.com/Sylius/CustomerOrderCancellationPlugin')); - $this->plugins->add(new PluginInfo('Customer Reorder', 'Convenient reordering for the customers from the `My account` section.', 'https://github.com/Sylius/CustomerReorderPlugin')); - $this->plugins->add(new PluginInfo('Invoicing', 'Automatised, basic invoicing system for orders.', 'https://github.com/Sylius/InvoicingPlugin')); - $this->plugins->add(new PluginInfo('Refund', 'Full and partial refunds of items and/or shipping costs including Credit Memos.', 'https://github.com/Sylius/RefundPlugin')); - $this->plugins->add(new PluginInfo('CMS', 'This plugin allows you to add dynamic blocks with images, text or HTML to your storefront as well as pages and FAQs section.', 'https://github.com/BitBagCommerce/SyliusCmsPlugin')); } } diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/AbstractInstallCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/AbstractInstallCommand.php new file mode 100644 index 0000000000..87b70c75b8 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/AbstractInstallCommand.php @@ -0,0 +1,133 @@ +getApplication(); + $application->setCatchExceptions(false); + + $this->commandExecutor = new CommandExecutor($input, $output, $application); + } + + /** + * @return object + */ + protected function get(string $id) + { + return $this->getContainer()->get($id); + } + + protected function getEnvironment(): string + { + return (string) $this->getContainer()->getParameter('kernel.environment'); + } + + protected function isDebug(): bool + { + return (bool) $this->getContainer()->getParameter('kernel.debug'); + } + + /** + * @param array $headers + * @param array $rows + */ + protected function renderTable(array $headers, array $rows, OutputInterface $output): void + { + $table = new Table($output); + + $table + ->setHeaders($headers) + ->setRows($rows) + ->render() + ; + } + + protected function createProgressBar(OutputInterface $output, int $length = 10): ProgressBar + { + $progress = new ProgressBar($output); + $progress->setBarCharacter(''); + $progress->setEmptyBarCharacter(' '); + $progress->setProgressCharacter(''); + + $progress->start($length); + + return $progress; + } + + /** @param array $commands */ + protected function runCommands(array $commands, OutputInterface $output, bool $displayProgress = true): void + { + $progress = $this->createProgressBar($displayProgress ? $output : new NullOutput(), count($commands)); + + foreach ($commands as $key => $value) { + if (is_string($key)) { + $command = $key; + $parameters = $value; + } else { + $command = $value; + $parameters = []; + } + + $this->commandExecutor->runCommand($command, $parameters); + + // PDO does not always close the connection after Doctrine commands. + // See https://github.com/symfony/symfony/issues/11750. + /** @var EntityManagerInterface $entityManager */ + $entityManager = $this->getContainer()->get('doctrine')->getManager(); + $entityManager->getConnection()->close(); + + $progress->advance(); + } + + $progress->finish(); + } + + protected function ensureDirectoryExistsAndIsWritable(string $directory, OutputInterface $output): void + { + $checker = $this->getContainer()->get('sylius.installer.checker.command_directory'); + $checker->setCommandName($this->getName()); + + $checker->ensureDirectoryExists($directory, $output); + $checker->ensureDirectoryIsWritable($directory, $output); + } +} + +class_alias(AbstractInstallCommand::class, '\Sylius\Bundle\CoreBundle\Command\AbstractInstallCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/CancelUnpaidOrdersCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/CancelUnpaidOrdersCommand.php new file mode 100644 index 0000000000..2552ccb5e3 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/CancelUnpaidOrdersCommand.php @@ -0,0 +1,52 @@ +setDescription( + 'Removes order that have been unpaid for a configured period. Configuration parameter - sylius_order.order_expiration_period.', + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $expirationTime = $this->getContainer()->getParameter('sylius_order.order_expiration_period'); + $output->writeln(sprintf( + 'Command will cancel orders that have been unpaid for %s.', + (string) $expirationTime, + )); + + $unpaidCartsStateUpdater = $this->getContainer()->get('sylius.unpaid_orders_state_updater'); + $unpaidCartsStateUpdater->cancel(); + + $this->getContainer()->get('sylius.manager.order')->flush(); + + $output->writeln('Unpaid orders have been canceled'); + + return 0; + } +} + +class_alias(CancelUnpaidOrdersCommand::class, '\Sylius\Bundle\CoreBundle\Command\CancelUnpaidOrdersCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/CheckRequirementsCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/CheckRequirementsCommand.php new file mode 100644 index 0000000000..403939a9a8 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/CheckRequirementsCommand.php @@ -0,0 +1,52 @@ +setDescription('Checks if all Sylius requirements are satisfied.') + ->setHelp( + <<%command.name% command checks system requirements. +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $fulfilled = $this->getContainer()->get('sylius.installer.checker.sylius_requirements')->check($input, $output); + + if (!$fulfilled) { + throw new RuntimeException( + 'Some system requirements are not fulfilled. Please check output messages and fix them.', + ); + } + + $output->writeln('Success! Your system can run Sylius properly.'); + + return 0; + } +} + +class_alias(CheckRequirementsCommand::class, '\Sylius\Bundle\CoreBundle\Command\CheckRequirementsCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/InformAboutGUSCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/InformAboutGUSCommand.php new file mode 100644 index 0000000000..f69795f26d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/InformAboutGUSCommand.php @@ -0,0 +1,53 @@ +setDescription('Informs about Sylius internal statistical service'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $style = new SymfonyStyle($input, $output); + + $style->note( + [ + 'For purely statistical purposes and in order to inform you about important updates and security patches, Sylius might send non-sensitive data to our servers. We send:', + '* Hostname', + '* User-agent', + '* Locale', + '* Environment (test, dev or prod)', + '* Currently used Sylius version', + '* Date of the last contact', + 'If you do not consent please follow this cookbook article:', + 'https://docs.sylius.com/en/latest/cookbook/configuration/disabling-admin-notifications.html', + 'That being said, every time we get a notification about a new site deployed with Sylius, it brings a huge smile to our faces and motivates us to continue our Open Source work.', + ], + ); + + return 0; + } +} + +class_alias(InformAboutGUSCommand::class, '\Sylius\Bundle\CoreBundle\Command\InformAboutGUSCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/InstallAssetsCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallAssetsCommand.php new file mode 100644 index 0000000000..d2db3942af --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallAssetsCommand.php @@ -0,0 +1,63 @@ +setDescription('Installs all Sylius assets.') + ->setHelp( + <<%command.name% command downloads and installs all Sylius media assets. +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln(sprintf( + 'Installing Sylius assets for environment %s.', + $this->getEnvironment(), + )); + + try { + $publicDir = $this->getContainer()->getParameter('sylius_core.public_dir'); + + $this->ensureDirectoryExistsAndIsWritable($publicDir . '/assets/', $output); + $this->ensureDirectoryExistsAndIsWritable($publicDir . '/bundles/', $output); + } catch (\RuntimeException $exception) { + $output->writeln($exception->getMessage()); + + return 1; + } + + $commands = [ + 'assets:install' => ['target' => $publicDir], + ]; + + $this->runCommands($commands, $output); + + return 0; + } +} + +class_alias(InstallAssetsCommand::class, '\Sylius\Bundle\CoreBundle\Command\InstallAssetsCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/InstallCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallCommand.php new file mode 100644 index 0000000000..c421ada04d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallCommand.php @@ -0,0 +1,139 @@ +> */ + private array $commands = [ + [ + 'command' => 'sylius:install:check-requirements', + 'message' => 'Checking system requirements.', + ], + [ + 'command' => 'sylius:install:database', + 'message' => 'Setting up the database.', + ], + [ + 'command' => 'sylius:install:setup', + 'message' => 'Shop configuration.', + ], + [ + 'command' => 'sylius:install:jwt-setup', + 'message' => 'Configuring JWT token.', + ], + [ + 'command' => 'sylius:install:assets', + 'message' => 'Installing assets.', + ], + [ + 'command' => 'cache:clear', + 'message' => 'Clearing cache.', + ], + ]; + + protected function configure(): void + { + $this + ->setDescription('Installs Sylius in your preferred environment.') + ->setHelp( + <<%command.name% command installs Sylius. +EOT + ) + ->addOption('fixture-suite', 's', InputOption::VALUE_OPTIONAL, 'Load specified fixture suite during install', null) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $suite = $input->getOption('fixture-suite'); + + $outputStyle = new SymfonyStyle($input, $output); + $outputStyle->writeln('Installing Sylius...'); + $outputStyle->writeln($this->getSyliusLogo()); + + $this->ensureDirectoryExistsAndIsWritable((string) $this->getContainer()->getParameter('kernel.cache_dir'), $output); + + $errored = false; + foreach ($this->commands as $step => $command) { + try { + $outputStyle->newLine(); + $outputStyle->section(sprintf( + 'Step %d of %d. %s', + $step + 1, + count($this->commands), + $command['message'], + )); + + $parameters = []; + if ('sylius:install:database' === $command['command'] && null !== $suite) { + $parameters['--fixture-suite'] = $suite; + } + + $this->commandExecutor->runCommand($command['command'], $parameters, $output); + } catch (RuntimeException) { + $errored = true; + } + } + + $outputStyle->newLine(2); + $outputStyle->success($this->getProperFinalMessage($errored)); + $outputStyle->writeln('You can now open your store at the following path under the website root: /'); + + return $errored ? 1 : 0; + } + + private function getProperFinalMessage(bool $errored): string + { + if ($errored) { + return 'Sylius has been installed, but some error occurred.'; + } + + return 'Sylius has been successfully installed.'; + } + + private function getSyliusLogo(): string + { + return ' + , + ,;:, + `;;;.:` + `::;` :` + :::` ` .\'++: \'\'. \'. + `::: :+\',;+\' :+; `+. + :::: +\' :\' `+; + `:::, \'+` ++ :+.`+; `++. ;+\' \'\' ,++++. + ,:::` `++\'. .+: `+\' `+; .+, ;+ +\' +; \'\' + ::::` ,+++. \'+` :+. `+; `+, ;+ +\' \'+. + ,. .:::: .++` `+: +\' `+; `+, ;+ +\' `;++; +`;;.:::` ::::: :+. \'+,+. `+; `+, ;+ `+\' .++ + .;;;;;;::`.::::, +\'` `++ `++\' `+; `+: :+. `++\' \'. ;+ + ,;;;;;;;;;::::: .+++++` ;+, ++; ++, `\'+++,\'+\' :++++, + ,;;;;;;;;;:::` ;\' + :;;;;;;;;;:, :.:+, + ;;;;;;;;;: ;++,' + ; + } +} + +class_alias(InstallCommand::class, '\Sylius\Bundle\CoreBundle\Command\InstallCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/InstallDatabaseCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallDatabaseCommand.php new file mode 100644 index 0000000000..d4fd111269 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallDatabaseCommand.php @@ -0,0 +1,67 @@ +setDescription('Install Sylius database.') + ->setHelp( + <<%command.name% command creates Sylius database. +EOT + ) + ->addOption('fixture-suite', 's', InputOption::VALUE_OPTIONAL, 'Load specified fixture suite during install', null) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $suite = $input->getOption('fixture-suite'); + + $outputStyle = new SymfonyStyle($input, $output); + $outputStyle->writeln(sprintf( + 'Creating Sylius database for environment %s.', + $this->getEnvironment(), + )); + + $commands = $this + ->getContainer() + ->get('sylius.commands_provider.database_setup') + ->getCommands($input, $output, $this->getHelper('question')) + ; + + $this->runCommands($commands, $output); + $outputStyle->newLine(); + + $parameters = []; + if (null !== $suite) { + $parameters['--fixture-suite'] = $suite; + } + $this->commandExecutor->runCommand('sylius:install:sample-data', $parameters, $output); + + return 0; + } +} + +class_alias(InstallDatabaseCommand::class, '\Sylius\Bundle\CoreBundle\Command\InstallDatabaseCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/InstallSampleDataCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallSampleDataCommand.php new file mode 100644 index 0000000000..5c4d62d29f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/InstallSampleDataCommand.php @@ -0,0 +1,89 @@ +setDescription('Install sample data into Sylius.') + ->setHelp( + <<%command.name% command loads the sample data for Sylius. +EOT + ) + ->addOption('fixture-suite', 's', InputOption::VALUE_OPTIONAL, 'Load specified fixture suite during install', null) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var QuestionHelper $questionHelper */ + $questionHelper = $this->getHelper('question'); + $suite = $input->getOption('fixture-suite'); + + $outputStyle = new SymfonyStyle($input, $output); + $outputStyle->newLine(); + $outputStyle->writeln(sprintf( + 'Loading sample data for environment %s from suite %s.', + $this->getEnvironment(), + $suite ?? 'default', + )); + $outputStyle->writeln('Warning! This action will erase your database.'); + + if (!$questionHelper->ask($input, $output, new ConfirmationQuestion('Continue? (y/N) ', null !== $suite))) { + $outputStyle->writeln('Cancelled loading sample data.'); + + return 0; + } + + try { + $publicDir = $this->getContainer()->getParameter('sylius_core.public_dir'); + + $this->ensureDirectoryExistsAndIsWritable($publicDir . '/media/', $output); + $this->ensureDirectoryExistsAndIsWritable($publicDir . '/media/image/', $output); + } catch (\RuntimeException $exception) { + $outputStyle->writeln($exception->getMessage()); + + return 1; + } + + $parameters = ['--no-interaction' => true]; + + if (null !== $suite) { + $parameters['suite'] = $suite; + } + + $commands = [ + 'sylius:fixtures:load' => $parameters, + ]; + + $this->runCommands($commands, $output); + $outputStyle->newLine(2); + + return 0; + } +} + +class_alias(InstallSampleDataCommand::class, '\Sylius\Bundle\CoreBundle\Command\InstallSampleDataCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/JwtConfigurationCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/JwtConfigurationCommand.php new file mode 100644 index 0000000000..ca90904677 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/JwtConfigurationCommand.php @@ -0,0 +1,62 @@ +setDescription('Setup JWT token') + ->setHelp( + <<%command.name% command generates JWT token. +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + /** @var HelperInterface $helper */ + $helper = $this->getHelper('question'); + + $output->writeln('Generating JWT token for Sylius API'); + + $question = new ConfirmationQuestion('Do you want to generate JWT token? (y/N)', false); + + Assert::isInstanceOf($helper, QuestionHelper::class); + if (!$helper->ask($input, $output, $question)) { + return 0; + } + + $this->commandExecutor->runCommand('lexik:jwt:generate-keypair', ['--overwrite' => true], $output); + + $output->writeln('Please, remember to enable Sylius API'); + $output->writeln('https://docs.sylius.com/en/1.10/book/api/introduction.html'); + + return 0; + } +} + +class_alias(JwtConfigurationCommand::class, '\Sylius\Bundle\CoreBundle\Command\JwtConfigurationCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/Model/PluginInfo.php b/src/Sylius/Bundle/CoreBundle/Console/Command/Model/PluginInfo.php new file mode 100644 index 0000000000..5213551663 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/Model/PluginInfo.php @@ -0,0 +1,38 @@ +name; + } + + public function description(): string + { + return $this->description; + } + + public function url(): string + { + return $this->url; + } +} + +class_alias(PluginInfo::class, '\Sylius\Bundle\CoreBundle\Command\Model\PluginInfo'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/SetupCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/SetupCommand.php new file mode 100644 index 0000000000..43b95ef052 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/SetupCommand.php @@ -0,0 +1,217 @@ +setDescription('Sylius configuration setup.') + ->setHelp( + <<%command.name% command allows user to configure basic Sylius data. +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $currency = $this->getContainer()->get('sylius.setup.currency')->setup($input, $output, $this->getHelper('question')); + $locale = $this->getContainer()->get('sylius.setup.locale')->setup($input, $output, $this->getHelper('question')); + $this->getContainer()->get('sylius.setup.channel')->setup($locale, $currency); + $this->setupAdministratorUser($input, $output, $locale->getCode()); + + return 0; + } + + protected function setupAdministratorUser(InputInterface $input, OutputInterface $output, string $localeCode): void + { + $outputStyle = new SymfonyStyle($input, $output); + $outputStyle->writeln('Create your administrator account.'); + + $userManager = $this->getContainer()->get('sylius.manager.admin_user'); + $userFactory = $this->getContainer()->get('sylius.factory.admin_user'); + + try { + $user = $this->configureNewUser($userFactory->createNew(), $input, $output); + } catch (\InvalidArgumentException) { + return; + } + + $user->setEnabled(true); + $user->setLocaleCode($localeCode); + + $userManager->persist($user); + $userManager->flush(); + + $outputStyle->writeln('Administrator account successfully registered.'); + $outputStyle->newLine(); + } + + private function configureNewUser( + AdminUserInterface $user, + InputInterface $input, + OutputInterface $output, + ): AdminUserInterface { + $userRepository = $this->getAdminUserRepository(); + + if ($input->getOption('no-interaction')) { + Assert::null($userRepository->findOneByEmail('sylius@example.com')); + + $user->setEmail('sylius@example.com'); + $user->setUsername('sylius'); + $user->setPlainPassword('sylius'); + + return $user; + } + + $email = $this->getAdministratorEmail($input, $output); + $user->setEmail($email); + $user->setUsername($this->getAdministratorUsername($input, $output, $email)); + $user->setPlainPassword($this->getAdministratorPassword($input, $output)); + + return $user; + } + + private function createEmailQuestion(): Question + { + return (new Question('E-mail: ')) + ->setValidator( + /** + * @param mixed $value + */ + function ($value): string { + /** @var ConstraintViolationListInterface $errors */ + $errors = $this->getContainer()->get('validator')->validate((string) $value, [new Email(), new NotBlank()]); + foreach ($errors as $error) { + throw new \DomainException((string) $error->getMessage()); + } + + return $value; + }, + ) + ->setMaxAttempts(3) + ; + } + + private function getAdministratorEmail(InputInterface $input, OutputInterface $output): string + { + /** @var QuestionHelper $questionHelper */ + $questionHelper = $this->getHelper('question'); + $userRepository = $this->getAdminUserRepository(); + + do { + $question = $this->createEmailQuestion(); + $email = $questionHelper->ask($input, $output, $question); + $exists = null !== $userRepository->findOneByEmail($email); + + if ($exists) { + $output->writeln('E-Mail is already in use!'); + } + } while ($exists); + + return $email; + } + + private function getAdministratorUsername(InputInterface $input, OutputInterface $output, string $email): string + { + /** @var QuestionHelper $questionHelper */ + $questionHelper = $this->getHelper('question'); + $userRepository = $this->getAdminUserRepository(); + + do { + $question = new Question('Username (press enter to use email): ', $email); + $username = $questionHelper->ask($input, $output, $question); + $exists = null !== $userRepository->findOneBy(['username' => $username]); + + if ($exists) { + $output->writeln('Username is already in use!'); + } + } while ($exists); + + return $username; + } + + private function getAdministratorPassword(InputInterface $input, OutputInterface $output): string + { + /** @var QuestionHelper $questionHelper */ + $questionHelper = $this->getHelper('question'); + $validator = $this->getPasswordQuestionValidator(); + + do { + $passwordQuestion = $this->createPasswordQuestion('Choose password:', $validator); + $confirmPasswordQuestion = $this->createPasswordQuestion('Confirm password:', $validator); + + $password = $questionHelper->ask($input, $output, $passwordQuestion); + $repeatedPassword = $questionHelper->ask($input, $output, $confirmPasswordQuestion); + + if ($repeatedPassword !== $password) { + $output->writeln('Passwords do not match!'); + } + } while ($repeatedPassword !== $password); + + return $password; + } + + private function getPasswordQuestionValidator(): \Closure + { + return + /** @param mixed $value */ + function ($value): string { + /** @var ConstraintViolationListInterface $errors */ + $errors = $this->getContainer()->get('validator')->validate($value, [new NotBlank()]); + foreach ($errors as $error) { + throw new \DomainException((string) $error->getMessage()); + } + + return $value; + } + ; + } + + private function createPasswordQuestion(string $message, \Closure $validator): Question + { + return (new Question($message)) + ->setValidator($validator) + ->setMaxAttempts(3) + ->setHidden(true) + ->setHiddenFallback(false) + ; + } + + /** @return UserRepositoryInterface */ + private function getAdminUserRepository(): UserRepositoryInterface + { + return $this->getContainer()->get('sylius.repository.admin_user'); + } +} + +class_alias(SetupCommand::class, '\Sylius\Bundle\CoreBundle\Command\SetupCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Console/Command/ShowAvailablePluginsCommand.php b/src/Sylius/Bundle/CoreBundle/Console/Command/ShowAvailablePluginsCommand.php new file mode 100644 index 0000000000..d1841cc466 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Console/Command/ShowAvailablePluginsCommand.php @@ -0,0 +1,66 @@ + */ + private Collection $plugins; + + protected function configure(): void + { + $this->setDescription('Shows official Sylius Plugins'); + $this->configurePlugins(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln('Available official plugins and selected community plugins:'); + + $pluginTable = new TableRenderer($output); + $pluginTable->setHeaders(['Plugin', 'Description', 'URL']); + + foreach ($this->plugins as $plugin) { + $pluginTable->addRow([sprintf('%s', $plugin->name()), $plugin->description(), $plugin->url()]); + } + + $pluginTable->render(); + + return 0; + } + + private function configurePlugins(): void + { + $this->plugins = new ArrayCollection(); + + $this->plugins->add(new PluginInfo('Admin Order Creation', 'Creating (and copying) orders in the administration panel.', 'https://github.com/Sylius/AdminOrderCreationPlugin')); + $this->plugins->add(new PluginInfo('Customer Order Cancellation', 'Allows customers to quickly cancel their unpaid and unshipped orders.', 'https://github.com/Sylius/CustomerOrderCancellationPlugin')); + $this->plugins->add(new PluginInfo('Customer Reorder', 'Convenient reordering for the customers from the `My account` section.', 'https://github.com/Sylius/CustomerReorderPlugin')); + $this->plugins->add(new PluginInfo('Invoicing', 'Automatised, basic invoicing system for orders.', 'https://github.com/Sylius/InvoicingPlugin')); + $this->plugins->add(new PluginInfo('Refund', 'Full and partial refunds of items and/or shipping costs including Credit Memos.', 'https://github.com/Sylius/RefundPlugin')); + $this->plugins->add(new PluginInfo('CMS', 'This plugin allows you to add dynamic blocks with images, text or HTML to your storefront as well as pages and FAQs section.', 'https://github.com/BitBagCommerce/SyliusCmsPlugin')); + } +} + +class_alias(ShowAvailablePluginsCommand::class, '\Sylius\Bundle\CoreBundle\Command\ShowAvailablePluginsCommand'); diff --git a/src/Sylius/Bundle/CoreBundle/Controller/OrderController.php b/src/Sylius/Bundle/CoreBundle/Controller/OrderController.php index cd06356ab2..a148546fe0 100644 --- a/src/Sylius/Bundle/CoreBundle/Controller/OrderController.php +++ b/src/Sylius/Bundle/CoreBundle/Controller/OrderController.php @@ -16,6 +16,9 @@ namespace Sylius\Bundle\CoreBundle\Controller; use FOS\RestBundle\View\View; use Sylius\Bundle\OrderBundle\Controller\OrderController as BaseOrderController; use Sylius\Component\Core\Repository\OrderRepositoryInterface; +use Sylius\Component\Order\SyliusCartEvents; +use Sylius\Component\Resource\ResourceActions; +use Symfony\Component\EventDispatcher\GenericEvent; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Webmozart\Assert\Assert; @@ -35,6 +38,13 @@ class OrderController extends BaseOrderController $cart = $orderRepository->findCartForSummary($cart->getId()); } + $this->getEventDispatcher()->dispatch(new GenericEvent($cart), SyliusCartEvents::CART_SUMMARY); + $event = $this->eventDispatcher->dispatch(ResourceActions::SHOW, $configuration, $cart); + $eventResponse = $event->getResponse(); + if (null !== $eventResponse) { + return $eventResponse; + } + if (!$configuration->isHtmlRequest()) { return $this->viewHandler->handle($configuration, View::create($cart)); } diff --git a/src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/BackwardsCompatibility/CancelOrderStateMachineCallbackPass.php b/src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/BackwardsCompatibility/CancelOrderStateMachineCallbackPass.php index 56912d0893..3089ff789f 100644 --- a/src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/BackwardsCompatibility/CancelOrderStateMachineCallbackPass.php +++ b/src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/BackwardsCompatibility/CancelOrderStateMachineCallbackPass.php @@ -28,13 +28,12 @@ final class CancelOrderStateMachineCallbackPass implements CompilerPassInterface $smConfigs = $container->getParameter('sm.configs'); if (isset($smConfigs['sylius_order']['callbacks']['after']['sylis_cancel_order'])) { - trigger_error( - sprintf( - 'Callback "%s" was renamed to "%s". The old name will be removed in Sylius 2.0, use the new name to override it.', - 'winzou_state_machine.sylius_order.callbacks.after.sylis_cancel_order', - 'winzou_state_machine.sylius_order.callbacks.after.sylius_cancel_order', - ), - \E_USER_DEPRECATED, + trigger_deprecation( + 'sylius/core-bundle', + '1.11', + 'Callback "%s" was renamed to "%s". The old name will be removed in Sylius 2.0, use the new name to override it.', + 'winzou_state_machine.sylius_order.callbacks.after.sylis_cancel_order', + 'winzou_state_machine.sylius_order.callbacks.after.sylius_cancel_order', ); $smConfigs['sylius_order']['callbacks']['after']['sylius_cancel_order'] = $smConfigs['sylius_order']['callbacks']['after']['sylis_cancel_order']; diff --git a/src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/CheckStatisticsOrdersTotalsProviderTypePass.php b/src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/CheckStatisticsOrdersTotalsProviderTypePass.php new file mode 100644 index 0000000000..d4b39abc7e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/CheckStatisticsOrdersTotalsProviderTypePass.php @@ -0,0 +1,46 @@ +hasParameter('sylius_core.orders_statistics.intervals_map')) { + return; + } + + $intervalsTypes = $container->getParameter('sylius_core.orders_statistics.intervals_map'); + $ordersTotalsProviderTypes = []; + + foreach ($container->findTaggedServiceIds('sylius.statistics.orders_totals_provider') as $attributes) { + foreach ($attributes as $attribute) { + if (!isset($attribute['type'])) { + throw new \InvalidArgumentException('Tagged orders totals providers need to have `type` attribute.'); + } + + $ordersTotalsProviderTypes[] = $attribute['type']; + } + } + + foreach ($intervalsTypes as $type => $interval) { + if (!in_array($type, $ordersTotalsProviderTypes, true)) { + throw new \InvalidArgumentException(sprintf('There is no orders totals provider for interval type "%s"', $type)); + } + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPass.php b/src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPass.php new file mode 100644 index 0000000000..6d2e306765 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPass.php @@ -0,0 +1,95 @@ + 'Sylius\Component\Core\Checker\ProductVariantLowestPriceDisplayCheckerInterface', + 'Sylius\PriceHistoryPlugin\Application\CommandDispatcher' => 'Sylius\Bundle\CoreBundle\PriceHistory\CommandDispatcher', + 'Sylius\PriceHistoryPlugin\Application\CommandHandler' => 'Sylius\Bundle\CoreBundle\PriceHistory\CommandHandler', + 'Sylius\PriceHistoryPlugin\Application\Logger' => 'Sylius\Bundle\CoreBundle\PriceHistory\Logger', + 'Sylius\PriceHistoryPlugin\Application\Processor' => 'Sylius\Bundle\CoreBundle\PriceHistory\Processor', + 'Sylius\PriceHistoryPlugin\Application\Remover' => 'Sylius\Bundle\CoreBundle\PriceHistory\Remover', + 'Sylius\PriceHistoryPlugin\Application\Templating\Helper\PriceHelper' => 'sylius.templating.helper.price', + 'Sylius\PriceHistoryPlugin\Domain\Factory\ChannelFactory' => 'sylius.custom_factory.channel', + 'Sylius\PriceHistoryPlugin\Infrastructure\Cli\Command\ClearPriceHistoryCommand' => 'Sylius\Bundle\CoreBundle\PriceHistory\Cli\Command\ClearPriceHistoryCommand', + 'Sylius\PriceHistoryPlugin\Infrastructure\EntityObserver' => 'Sylius\Bundle\CoreBundle\PriceHistory\EntityObserver', + 'Sylius\PriceHistoryPlugin\Infrastructure\EventSubscriber' => 'Sylius\Bundle\CoreBundle\PriceHistory\EventSubscriber', + 'Sylius\PriceHistoryPlugin\Infrastructure\EventListener' => 'Sylius\Bundle\CoreBundle\PriceHistory\EventListener', + 'Sylius\PriceHistoryPlugin\Infrastructure\Form\Extension\ChannelTypeExtension' => 'sylius.form.extension.type.channel', + 'Sylius\PriceHistoryPlugin\Infrastructure\Form\Type\ChannelPriceHistoryConfigType' => 'sylius.form.type.channel_price_history_config', + 'Sylius\PriceHistoryPlugin\Infrastructure\Provider\ProductVariantsPricesProvider' => 'sylius.provider.product_variants_prices', + 'Sylius\PriceHistoryPlugin\Infrastructure\Twig\PriceExtension' => 'sylius.twig.extension.price', + 'sylius_price_history.controller.channel_price_history_config' => 'sylius.controller.channel_price_history_config', + 'sylius_price_history.controller.channel_pricing_log_entry' => 'sylius.controller.channel_pricing_log_entry', + 'sylius_price_history.factory.channel_price_history_config' => 'sylius.factory.channel_price_history_config', + 'sylius_price_history.factory.channel_pricing_log_entry' => 'sylius.factory.channel_pricing_log_entry', + 'sylius_price_history.form.type.channel_price_history_config' => 'sylius.form.type.channel_price_history_config', + 'sylius_price_history.form.type.channel_pricing_log_entry' => 'sylius.form.type.channel_pricing_log_entry', + 'sylius_price_history.manager.channel_price_history_config' => 'sylius.manager.channel_price_history_config', + 'sylius_price_history.manager.channel_pricing_log_entry' => 'sylius.manager.channel_pricing_log_entry', + 'sylius_price_history.repository.channel_price_history_config' => 'sylius.repository.channel_price_history_config', + 'sylius_price_history.repository.channel_pricing_log_entry' => 'sylius.repository.channel_pricing_log_entry', + ]; + + public function process(ContainerBuilder $container): void + { + foreach ($container->getDefinitions() as $serviceName => $definition) { + if ($this->shouldHaveLegacyAlias($container, $serviceName)) { + $this->addLegacyAlias($container, $serviceName); + } + } + } + + private function shouldHaveLegacyAlias(ContainerBuilder $container, string $serviceName): bool + { + foreach (self::PLUGIN_TO_SYLIUS_PREFIXES_MAP as $legacyPrefix => $syliusPrefix) { + $legacyServiceName = $this->getLegacyServiceName($serviceName); + + if (str_starts_with($serviceName, $syliusPrefix) && !$container->has($legacyServiceName)) { + return true; + } + } + + return false; + } + + private function addLegacyAlias(ContainerBuilder $container, string $serviceName): void + { + $legacyServiceName = $this->getLegacyServiceName($serviceName); + + $container + ->setAlias($legacyServiceName, $serviceName) + ->setPublic(true) + ->setDeprecated( + 'sylius/sylius', + '1.13', + sprintf('Alias %%alias_id%% is deprecated. Consider using the %s service.', $serviceName), + ) + ; + } + + private function getLegacyServiceName(string $name): string + { + return str_replace( + array_values(self::PLUGIN_TO_SYLIUS_PREFIXES_MAP), + array_keys(self::PLUGIN_TO_SYLIUS_PREFIXES_MAP), + $name, + ); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/CoreBundle/DependencyInjection/Configuration.php index b89277adcf..ff7aeac4a8 100644 --- a/src/Sylius/Bundle/CoreBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/CoreBundle/DependencyInjection/Configuration.php @@ -14,14 +14,21 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\DependencyInjection; use Sylius\Bundle\CoreBundle\Controller\ProductTaxonController; +use Sylius\Bundle\CoreBundle\Doctrine\ORM\ChannelPricingLogEntryRepository; use Sylius\Bundle\CoreBundle\Form\Type\Product\ChannelPricingType; use Sylius\Bundle\CoreBundle\Form\Type\ShopBillingDataType; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; +use Sylius\Bundle\ResourceBundle\Form\Type\DefaultResourceType; use Sylius\Bundle\ResourceBundle\SyliusResourceBundle; +use Sylius\Component\Core\Factory\ChannelPricingLogEntryFactory; use Sylius\Component\Core\Model\AvatarImage; use Sylius\Component\Core\Model\AvatarImageInterface; +use Sylius\Component\Core\Model\ChannelPriceHistoryConfig; +use Sylius\Component\Core\Model\ChannelPriceHistoryConfigInterface; use Sylius\Component\Core\Model\ChannelPricing; use Sylius\Component\Core\Model\ChannelPricingInterface; +use Sylius\Component\Core\Model\ChannelPricingLogEntry; +use Sylius\Component\Core\Model\ChannelPricingLogEntryInterface; use Sylius\Component\Core\Model\ProductImage; use Sylius\Component\Core\Model\ProductImageInterface; use Sylius\Component\Core\Model\ProductTaxon; @@ -47,6 +54,7 @@ final class Configuration implements ConfigurationInterface ->addDefaultsIfNotSet() ->children() ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() + ->scalarNode('autoconfigure_with_attributes')->defaultFalse()->end() ->booleanNode('prepend_doctrine_migrations')->defaultTrue()->end() ->booleanNode('shipping_address_based_taxation')->defaultFalse()->end() ->booleanNode('order_by_identifier')->defaultTrue()->end() @@ -54,6 +62,35 @@ final class Configuration implements ConfigurationInterface ->setDeprecated('sylius/sylius', '1.10', 'The "%path%.%node%" parameter is deprecated and will be removed in 2.0.') ->defaultFalse() ->end() + ->arrayNode('orders_statistics') + ->children() + ->arrayNode('intervals_map') + ->useAttributeAsKey('type') + ->arrayPrototype() + ->children() + ->scalarNode('interval') + ->cannotBeEmpty() + ->validate() + ->ifTrue( + function (mixed $interval) { + try { + new \DateInterval($interval); + + return false; + } catch (\Exception $e) { + return true; + } + }, + ) + ->thenInvalid('Invalid format for interval "%s". Expected a string compatible with DateInterval.') + ->end() + ->end() + ->scalarNode('period_format')->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() + ->end() ->arrayNode('catalog_promotions') ->addDefaultsIfNotSet() ->children() @@ -66,6 +103,18 @@ final class Configuration implements ConfigurationInterface ->end() ->end() ->end() + ->arrayNode('price_history') + ->addDefaultsIfNotSet() + ->children() + ->integerNode('batch_size') + ->defaultValue(100) + ->validate() + ->ifTrue(fn (int $batchSize): bool => $batchSize <= 0) + ->thenInvalid('Expected value bigger than 0, but got %s.') + ->end() + ->end() + ->end() + ->end() ->arrayNode('filesystem') ->addDefaultsIfNotSet() ->children() @@ -78,6 +127,16 @@ final class Configuration implements ConfigurationInterface ->end() ->end() ->end() + ->arrayNode('state_machine') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('default_adapter')->defaultValue('winzou_state_machine')->end() + ->arrayNode('graphs_to_adapters_mapping') + ->useAttributeAsKey('graph_name') + ->scalarPrototype()->end() + ->end() + ->end() + ->end() ->end() ; @@ -174,6 +233,23 @@ final class Configuration implements ConfigurationInterface ->end() ->end() ->end() + ->arrayNode('channel_pricing_log_entry') + ->addDefaultsIfNotSet() + ->children() + ->variableNode('options')->end() + ->arrayNode('classes') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('model')->defaultValue(ChannelPricingLogEntry::class)->cannotBeEmpty()->end() + ->scalarNode('interface')->defaultValue(ChannelPricingLogEntryInterface::class)->cannotBeEmpty()->end() + ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(ChannelPricingLogEntryRepository::class)->cannotBeEmpty()->end() + ->scalarNode('factory')->defaultValue(ChannelPricingLogEntryFactory::class)->end() + ->scalarNode('form')->defaultValue(DefaultResourceType::class)->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() ->arrayNode('shop_billing_data') ->addDefaultsIfNotSet() ->children() @@ -191,6 +267,23 @@ final class Configuration implements ConfigurationInterface ->end() ->end() ->end() + ->arrayNode('channel_price_history_config') + ->addDefaultsIfNotSet() + ->children() + ->variableNode('options')->end() + ->arrayNode('classes') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('model')->defaultValue(ChannelPriceHistoryConfig::class)->cannotBeEmpty()->end() + ->scalarNode('interface')->defaultValue(ChannelPriceHistoryConfigInterface::class)->cannotBeEmpty()->end() + ->scalarNode('controller')->defaultValue(ResourceController::class)->end() + ->scalarNode('factory')->defaultValue(Factory::class)->end() + ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('form')->defaultValue(DefaultResourceType::class)->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() ->end() ->end() ->end() diff --git a/src/Sylius/Bundle/CoreBundle/DependencyInjection/SyliusCoreExtension.php b/src/Sylius/Bundle/CoreBundle/DependencyInjection/SyliusCoreExtension.php index 365f52d41e..a88ff9728c 100644 --- a/src/Sylius/Bundle/CoreBundle/DependencyInjection/SyliusCoreExtension.php +++ b/src/Sylius/Bundle/CoreBundle/DependencyInjection/SyliusCoreExtension.php @@ -13,11 +13,20 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\DependencyInjection; -use InvalidArgumentException; +use Sylius\Bundle\CoreBundle\Attribute\AsCatalogPromotionApplicatorCriteria; +use Sylius\Bundle\CoreBundle\Attribute\AsCatalogPromotionPriceCalculator; +use Sylius\Bundle\CoreBundle\Attribute\AsEntityObserver; +use Sylius\Bundle\CoreBundle\Attribute\AsOrderItemsTaxesApplicator; +use Sylius\Bundle\CoreBundle\Attribute\AsOrderItemUnitsTaxesApplicator; +use Sylius\Bundle\CoreBundle\Attribute\AsOrdersTotalsProvider; +use Sylius\Bundle\CoreBundle\Attribute\AsProductVariantMapProvider; +use Sylius\Bundle\CoreBundle\Attribute\AsTaxCalculationStrategy; +use Sylius\Bundle\CoreBundle\Attribute\AsUriBasedSectionResolver; use Sylius\Bundle\ResourceBundle\DependencyInjection\Extension\AbstractResourceExtension; use Sylius\Component\Core\Filesystem\Adapter\FilesystemAdapterInterface; use Sylius\Component\Core\Filesystem\Adapter\FlysystemFilesystemAdapter; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -62,6 +71,8 @@ final class SyliusCoreExtension extends AbstractResourceExtension implements Pre $container->setParameter('sylius_core.taxation.shipping_address_based_taxation', $config['shipping_address_based_taxation']); $container->setParameter('sylius_core.order_by_identifier', $config['order_by_identifier']); $container->setParameter('sylius_core.catalog_promotions.batch_size', $config['catalog_promotions']['batch_size']); + $container->setParameter('sylius_core.price_history.batch_size', $config['price_history']['batch_size']); + $container->setParameter('sylius_core.orders_statistics.intervals_map', $config['orders_statistics']['intervals_map'] ?? []); /** @var string $env */ $env = $container->getParameter('kernel.environment'); @@ -81,12 +92,14 @@ final class SyliusCoreExtension extends AbstractResourceExtension implements Pre match ($config['filesystem']['adapter']) { 'default', 'flysystem' => FlysystemFilesystemAdapter::class, 'gaufrette' => 'Sylius\Component\Core\Filesystem\Adapter\GaufretteFilesystemAdapter', - default => throw new InvalidArgumentException(sprintf( + default => throw new \InvalidArgumentException(sprintf( 'Invalid filesystem adapter "%s" provided.', $config['filesystem']['adapter'], )), }, ); + + $this->registerAutoconfiguration($container); } public function prepend(ContainerBuilder $container): void @@ -98,6 +111,7 @@ final class SyliusCoreExtension extends AbstractResourceExtension implements Pre $this->prependHwiOauth($container); $this->prependDoctrineMigrations($container); $this->prependJmsSerializerIfAdminApiBundleIsNotPresent($container); + $this->prependSyliusOrderBundle($container, $config); } protected function getMigrationsNamespace(): string @@ -166,10 +180,21 @@ final class SyliusCoreExtension extends AbstractResourceExtension implements Pre ]); } + private function prependSyliusOrderBundle(ContainerBuilder $container, array $config): void + { + if (!$container->hasExtension('sylius_order')) { + return; + } + + $container->prependExtensionConfig('sylius_order', [ + 'autoconfigure_with_attributes' => $config['autoconfigure_with_attributes'] ?? false, + ]); + } + private function switchOrderProcessorsPriorities( Definition $firstServiceDefinition, Definition $secondServiceDefinition, - ) { + ): void { $firstServicePriority = $firstServiceDefinition->getTag('sylius.order_processor')[0]['priority']; $secondServicePriority = $secondServiceDefinition->getTag('sylius.order_processor')[0]['priority']; @@ -185,4 +210,74 @@ final class SyliusCoreExtension extends AbstractResourceExtension implements Pre ['priority' => $firstServicePriority], ); } + + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsCatalogPromotionApplicatorCriteria::class, + static function (ChildDefinition $definition, AsCatalogPromotionApplicatorCriteria $attribute): void { + $definition->addTag(AsCatalogPromotionApplicatorCriteria::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsCatalogPromotionPriceCalculator::class, + static function (ChildDefinition $definition, AsCatalogPromotionPriceCalculator $attribute): void { + $definition->addTag(AsCatalogPromotionPriceCalculator::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsEntityObserver::class, + static function (ChildDefinition $definition, AsEntityObserver $attribute): void { + $definition->addTag(AsEntityObserver::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsOrderItemsTaxesApplicator::class, + static function (ChildDefinition $definition, AsOrderItemsTaxesApplicator $attribute): void { + $definition->addTag(AsOrderItemsTaxesApplicator::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsOrderItemUnitsTaxesApplicator::class, + static function (ChildDefinition $definition, AsOrderItemUnitsTaxesApplicator $attribute): void { + $definition->addTag(AsOrderItemUnitsTaxesApplicator::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsProductVariantMapProvider::class, + static function (ChildDefinition $definition, AsProductVariantMapProvider $attribute): void { + $definition->addTag(AsProductVariantMapProvider::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsTaxCalculationStrategy::class, + static function (ChildDefinition $definition, AsTaxCalculationStrategy $attribute): void { + $definition->addTag(AsTaxCalculationStrategy::SERVICE_TAG, [ + 'type' => $attribute->getType(), + 'label' => $attribute->getLabel(), + 'priority' => $attribute->getPriority(), + ]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsUriBasedSectionResolver::class, + static function (ChildDefinition $definition, AsUriBasedSectionResolver $attribute): void { + $definition->addTag(AsUriBasedSectionResolver::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsOrdersTotalsProvider::class, + static function (ChildDefinition $definition, AsOrdersTotalsProvider $attribute): void { + $definition->addTag(AsOrdersTotalsProvider::SERVICE_TAG, ['type' => $attribute->getType()]); + }, + ); + } } diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/Migrations/AbstractMigration.php b/src/Sylius/Bundle/CoreBundle/Doctrine/Migrations/AbstractMigration.php index 044635df8c..d6f77f4d61 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/Migrations/AbstractMigration.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/Migrations/AbstractMigration.php @@ -20,12 +20,15 @@ abstract class AbstractMigration extends BaseAbstractMigration { public function preUp(Schema $schema): void { - $this->abortIf(!$this->isMySql(), 'Migration can only be executed safely on \'mysql\'.'); + if (!$this->isMySql()) { + $this->markAsExecuted($this->getVersion()); + $this->skipIf(true, 'This migration can only be executed on \'MySQL\'.'); + } } public function preDown(Schema $schema): void { - $this->abortIf(!$this->isMySql(), 'Migration can only be executed safely on \'mysql\'.'); + $this->skipIf(!$this->isMySql(), 'This migration can only be executed on \'MySQL\'.'); } protected function isMariaDb(): bool @@ -60,7 +63,20 @@ abstract class AbstractMigration extends BaseAbstractMigration return false; } - private function classExistsCaseSensitive(string $className): bool + private function getVersion(): string + { + return (new \ReflectionClass($this))->getName(); + } + + protected function markAsExecuted(string $version): void + { + $this->connection->executeQuery( + sprintf('INSERT INTO sylius_migrations (version, executed_at) VALUES (\'%s\', NOW())', $version), + ); + $this->connection->commit(); + } + + protected function classExistsCaseSensitive(string $className): bool { return class_exists(strtolower($className)) && (new \ReflectionClass($className))->getName() === $className; } diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/Migrations/AbstractPostgreSQLMigration.php b/src/Sylius/Bundle/CoreBundle/Doctrine/Migrations/AbstractPostgreSQLMigration.php new file mode 100644 index 0000000000..b6d892b0f0 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/Migrations/AbstractPostgreSQLMigration.php @@ -0,0 +1,55 @@ +isPostgreSQL()) { + $this->markAsExecuted($this->getVersion()); + $this->skipIf(true, 'This migration can only be executed on \'PostgreSQL\'.'); + } + } + + public function preDown(Schema $schema): void + { + $this->skipIf(!$this->isPostgreSQL(), 'This migration can only be executed on \'PostgreSQL\'.'); + } + + protected function isPostgreSQL(): bool + { + return + class_exists(PostgreSQLPlatform::class) && + is_a($this->connection->getDatabasePlatform(), PostgreSQLPlatform::class, true) + ; + } + + protected function markAsExecuted(string $version): void + { + $this->connection->executeQuery( + sprintf('INSERT INTO sylius_migrations (version, executed_at) VALUES (\'%s\', NOW())', $version), + ); + $this->connection->commit(); + } + + protected function getVersion(): string + { + return addslashes((new \ReflectionClass($this))->getName()); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AddressRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AddressRepository.php index 554fbe196c..bcca8d8b37 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AddressRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AddressRepository.php @@ -18,6 +18,11 @@ use Sylius\Component\Core\Model\AddressInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Repository\AddressRepositoryInterface; +/** + * @template T of AddressInterface + * + * @implements AddressRepositoryInterface + */ class AddressRepository extends EntityRepository implements AddressRepositoryInterface { public function findByCustomer(CustomerInterface $customer): array diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AttributeRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AttributeRepository.php index 5644d42f8b..2c609a9d04 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AttributeRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AttributeRepository.php @@ -16,12 +16,15 @@ namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Attribute\Model\AttributeInterface; use SyliusLabs\AssociationHydrator\AssociationHydrator; +/** + * @template T of AttributeInterface + */ class AttributeRepository extends EntityRepository { - /** @var AssociationHydrator */ - protected $associationHydrator; + protected AssociationHydrator $associationHydrator; public function __construct(EntityManager $entityManager, ClassMetadata $class) { diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AvatarImageRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AvatarImageRepository.php index 3168c75556..cd36f6be1e 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AvatarImageRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/AvatarImageRepository.php @@ -14,9 +14,15 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Core\Model\AvatarImageInterface; use Sylius\Component\Core\Model\ImageInterface; use Sylius\Component\Core\Repository\AvatarImageRepositoryInterface; +/** + * @template T of AvatarImageInterface + * + * @implements AvatarImageRepositoryInterface + */ final class AvatarImageRepository extends EntityRepository implements AvatarImageRepositoryInterface { public function findOneByOwnerId(string $id): ?ImageInterface diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ChannelPricingLogEntryRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ChannelPricingLogEntryRepository.php new file mode 100644 index 0000000000..ae538b494e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ChannelPricingLogEntryRepository.php @@ -0,0 +1,133 @@ + + */ +class ChannelPricingLogEntryRepository extends EntityRepository implements ChannelPricingLogEntryRepositoryInterface +{ + public function createByChannelPricingIdListQueryBuilder(mixed $channelPricingId): QueryBuilder + { + return $this->createQueryBuilder('o') + ->innerJoin('o.channelPricing', 'channelPricing') + ->andWhere('channelPricing = :channelPricingId') + ->orderBy('o.id', 'DESC') + ->setParameter('channelPricingId', $channelPricingId) + ; + } + + public function findOlderThan(\DateTimeInterface $date, ?int $limit = null): array + { + $queryBuilder = $this->createQueryBuilder('o') + ->andWhere('o.loggedAt < :date') + ->setParameter('date', $date) + ; + + if (null !== $limit) { + Assert::positiveInteger($limit); + $queryBuilder->setMaxResults($limit); + } + + return $queryBuilder->getQuery()->getResult(); + } + + public function findLatestOneByChannelPricing(ChannelPricingInterface $channelPricing): ?ChannelPricingLogEntryInterface + { + /** @var ChannelPricingLogEntryInterface|null $channelPricingLogEntry */ + $channelPricingLogEntry = $this->createQueryBuilder('o') + ->andWhere('o.channelPricing = :channelPricing') + ->setParameter('channelPricing', $channelPricing) + ->orderBy('o.id', 'DESC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult() + ; + + return $channelPricingLogEntry; + } + + public function findLowestPriceInPeriod( + int $latestChannelPricingLogEntryId, + ChannelPricingInterface $channelPricing, + \DateTimeInterface $startDate, + ): ?int { + $lowestPriceSetInPeriod = $this->findLowestPriceSetInPeriod($latestChannelPricingLogEntryId, $channelPricing, $startDate); + $latestPriceSetBeyondPeriod = $this->findLatestPriceSetBeyondPeriod($channelPricing, $startDate); + + if (null === $lowestPriceSetInPeriod) { + return $latestPriceSetBeyondPeriod; + } + + if (null === $latestPriceSetBeyondPeriod) { + return $lowestPriceSetInPeriod; + } + + if ($latestPriceSetBeyondPeriod < $lowestPriceSetInPeriod) { + return $latestPriceSetBeyondPeriod; + } + + return $lowestPriceSetInPeriod; + } + + private function findLowestPriceSetInPeriod( + int $latestChannelPricingLogEntryId, + ChannelPricingInterface $channelPricing, + \DateTimeInterface $startDate, + ): ?int { + /** @var ChannelPricingLogEntryInterface|null $channelPricingLogEntry */ + $channelPricingLogEntry = $this->createQueryBuilder('o') + ->andWhere('o.loggedAt >= :startDate') + ->andWhere('o.id != :channelPricingLogEntryId') + ->andWhere('o.channelPricing = :channelPricing') + ->setParameter('startDate', $startDate) + ->setParameter('channelPricing', $channelPricing) + ->setParameter('channelPricingLogEntryId', $latestChannelPricingLogEntryId) + ->orderBy('o.price', 'ASC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult() + ; + + return $channelPricingLogEntry?->getPrice(); + } + + private function findLatestPriceSetBeyondPeriod( + ChannelPricingInterface $channelPricing, + \DateTimeInterface $startDate, + ): ?int { + /** @var ChannelPricingLogEntryInterface|null $channelPricingLogEntry */ + $channelPricingLogEntry = $this->createQueryBuilder('o') + ->andWhere('o.loggedAt < :startDate') + ->andWhere('o.channelPricing = :channelPricing') + ->setParameter('startDate', $startDate) + ->setParameter('channelPricing', $channelPricing) + ->orderBy('o.id', 'DESC') + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult() + ; + + return $channelPricingLogEntry?->getPrice(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/CustomerRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/CustomerRepository.php index 2d9f6fc4f3..298e5207b2 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/CustomerRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/CustomerRepository.php @@ -14,8 +14,14 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Repository\CustomerRepositoryInterface; +/** + * @template T of CustomerInterface + * + * @implements CustomerRepositoryInterface + */ class CustomerRepository extends EntityRepository implements CustomerRepositoryInterface { public function countCustomers(): int diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderItemRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderItemRepository.php index 5ce246deb2..de43002ef6 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderItemRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderItemRepository.php @@ -13,11 +13,19 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; +use Doctrine\DBAL\Types\Types; use Sylius\Bundle\OrderBundle\Doctrine\ORM\OrderItemRepository as BaseOrderItemRepository; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderItemInterface; use Sylius\Component\Core\Repository\OrderItemRepositoryInterface; +/** + * @template T of OrderItemInterface + * + * @extends BaseOrderItemRepository + * + * @implements OrderItemRepositoryInterface + */ class OrderItemRepository extends BaseOrderItemRepository implements OrderItemRepositoryInterface { public function findOneByIdAndCustomer($id, CustomerInterface $customer): ?OrderItemInterface @@ -33,4 +41,20 @@ class OrderItemRepository extends BaseOrderItemRepository implements OrderItemRe ->getOneOrNullResult() ; } + + public function findOneByIdAndOrderTokenValue(int $id, string $tokenValue): ?OrderItemInterface + { + $queryBuilder = $this->createQueryBuilder('o') + ->innerJoin('o.order', 'ord') + ; + + $queryBuilder + ->andWhere($queryBuilder->expr()->eq('o.id', ':id')) + ->andWhere($queryBuilder->expr()->eq('ord.tokenValue', ':tokenValue')) + ->setParameter('id', $id, Types::BIGINT) + ->setParameter('tokenValue', $tokenValue, Types::STRING) + ; + + return $queryBuilder->getQuery()->getOneOrNullResult(); + } } diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderItemUnitRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderItemUnitRepository.php index 58f338e934..15ef15a7a0 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderItemUnitRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderItemUnitRepository.php @@ -18,6 +18,11 @@ use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderItemUnitInterface; use Sylius\Component\Core\Repository\OrderItemUnitRepositoryInterface; +/** + * @template T of OrderItemUnitInterface + * + * @implements OrderItemUnitRepositoryInterface + */ class OrderItemUnitRepository extends EntityRepository implements OrderItemUnitRepositoryInterface { public function findOneByCustomer($id, CustomerInterface $customer): ?OrderItemUnitInterface diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderRepository.php index f46d445dbb..643f801306 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderRepository.php @@ -26,11 +26,18 @@ use Sylius\Component\Core\OrderPaymentStates; use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Sylius\Component\Order\Model\OrderInterface as BaseOrderInterface; use SyliusLabs\AssociationHydrator\AssociationHydrator; +use Webmozart\Assert\Assert; +/** + * @template T of OrderInterface + * + * @extends BaseOrderRepository + * + * @implements OrderRepositoryInterface + */ class OrderRepository extends BaseOrderRepository implements OrderRepositoryInterface { - /** @var AssociationHydrator */ - protected $associationHydrator; + protected AssociationHydrator $associationHydrator; public function __construct(EntityManager $entityManager, ClassMetadata $class) { @@ -51,6 +58,12 @@ class OrderRepository extends BaseOrderRepository implements OrderRepositoryInte public function createSearchListQueryBuilder(): QueryBuilder { + trigger_deprecation( + 'sylius/core', + '1.13', + 'This method is deprecated and it will be removed in Sylius 2.0. Please use `createCriteriaAwareSearchListQueryBuilder` instead.', + ); + return $this->createListQueryBuilder() ->leftJoin('o.items', 'item') ->leftJoin('item.variant', 'variant') @@ -58,14 +71,57 @@ class OrderRepository extends BaseOrderRepository implements OrderRepositoryInte ; } + public function createCriteriaAwareSearchListQueryBuilder(?array $criteria): QueryBuilder + { + if ($criteria === null) { + return $this->createListQueryBuilder(); + } + + $hasProductCriteria = '' !== $criteria['product']; + $hasVariantCriteria = '' !== $criteria['variant']; + + $queryBuilder = $this->createListQueryBuilder(); + + if ($hasVariantCriteria || $hasProductCriteria) { + $queryBuilder + ->leftJoin('o.items', 'item') + ->leftJoin('item.variant', 'variant') + ; + } + + if ($hasProductCriteria) { + $queryBuilder + ->leftJoin('variant.product', 'product') + ; + } + + return $queryBuilder; + } + public function createByCustomerIdQueryBuilder($customerId): QueryBuilder { + trigger_deprecation( + 'sylius/core', + '1.13', + 'This method is deprecated and it will be removed in Sylius 2.0. Please use `createByCustomerIdCriteriaAwareQueryBuilder` instead.', + ); + return $this->createListQueryBuilder() ->andWhere('o.customer = :customerId') ->setParameter('customerId', $customerId) ; } + public function createByCustomerIdCriteriaAwareQueryBuilder(?array $criteria, string $customerId): QueryBuilder + { + $queryBuilder = $this->createCriteriaAwareSearchListQueryBuilder($criteria); + + return $queryBuilder + ->andWhere('o.customer = :customerId') + ->setParameter('customerId', $customerId) + ; + } + public function createByCustomerAndChannelIdQueryBuilder($customerId, $channelId): QueryBuilder { return $this->createQueryBuilder('o') @@ -127,7 +183,6 @@ class OrderRepository extends BaseOrderRepository implements OrderRepositoryInte public function countByCustomerAndCoupon( CustomerInterface $customer, PromotionCouponInterface $coupon, - bool $includeCancelled = false, ): int { $states = [OrderInterface::STATE_CART]; if ($coupon->isReusableFromCancelledOrders()) { @@ -254,21 +309,35 @@ class OrderRepository extends BaseOrderRepository implements OrderRepositoryInte \DateTimeInterface $startDate, \DateTimeInterface $endDate, ): int { - return (int) $this->createQueryBuilder('o') + return (int) $this->createPaidOrdersInChannelPlacedWithinDateRangeQueryBuilder($channel, $startDate, $endDate) ->select('SUM(o.total)') - ->andWhere('o.channel = :channel') - ->andWhere('o.paymentState = :state') - ->andWhere('o.checkoutCompletedAt >= :startDate') - ->andWhere('o.checkoutCompletedAt <= :endDate') - ->setParameter('channel', $channel) - ->setParameter('state', OrderPaymentStates::STATE_PAID) - ->setParameter('startDate', $startDate) - ->setParameter('endDate', $endDate) ->getQuery() ->getSingleScalarResult() ; } + public function getGroupedTotalPaidSalesForChannelInPeriod( + ChannelInterface $channel, + \DateTimeInterface $startDate, + \DateTimeInterface $endDate, + array $groupBy, + ): array { + $queryBuilder = $this->createPaidOrdersInChannelPlacedWithinDateRangeQueryBuilder($channel, $startDate, $endDate); + $queryBuilder->select('SUM(o.total) AS total'); + + foreach ($groupBy as $name => $select) { + $queryBuilder + ->addSelect($select) + ->addGroupBy($name) + ; + } + + return $queryBuilder + ->getQuery() + ->getArrayResult() + ; + } + public function countFulfilledByChannel(ChannelInterface $channel): int { return (int) $this->createQueryBuilder('o') @@ -329,9 +398,9 @@ class OrderRepository extends BaseOrderRepository implements OrderRepositoryInte ; } - public function findOrdersUnpaidSince(\DateTimeInterface $terminalDate): array + public function findOrdersUnpaidSince(\DateTimeInterface $terminalDate, ?int $limit = null): array { - return $this->createQueryBuilder('o') + $queryBuilder = $this->createQueryBuilder('o') ->where('o.checkoutState = :checkoutState') ->andWhere('o.paymentState = :paymentState') ->andWhere('o.state = :orderState') @@ -340,9 +409,14 @@ class OrderRepository extends BaseOrderRepository implements OrderRepositoryInte ->setParameter('paymentState', OrderPaymentStates::STATE_AWAITING_PAYMENT) ->setParameter('orderState', OrderInterface::STATE_NEW) ->setParameter('terminalDate', $terminalDate) - ->getQuery() - ->getResult() ; + + if (null !== $limit) { + Assert::positiveInteger($limit); + $queryBuilder->setMaxResults($limit); + } + + return $queryBuilder->getQuery()->getResult(); } public function findCartForSummary($id): ?OrderInterface @@ -467,4 +541,20 @@ class OrderRepository extends BaseOrderRepository implements OrderRepositoryInte ->getOneOrNullResult() ; } + + protected function createPaidOrdersInChannelPlacedWithinDateRangeQueryBuilder( + ChannelInterface $channel, + \DateTimeInterface $startDate, + \DateTimeInterface $endDate, + ): QueryBuilder { + return $this->createQueryBuilder('o') + ->andWhere('o.paymentState = :paymentState') + ->andWhere('o.channel = :channel') + ->andWhere('o.checkoutCompletedAt >= :startDate') + ->andWhere('o.checkoutCompletedAt <= :endDate') + ->setParameter('paymentState', OrderPaymentStates::STATE_PAID) + ->setParameter('channel', $channel) + ->setParameter('startDate', $startDate) + ->setParameter('endDate', $endDate); + } } diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PaymentMethodRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PaymentMethodRepository.php index c2c97eb901..6ee7582c3d 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PaymentMethodRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PaymentMethodRepository.php @@ -16,8 +16,16 @@ namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\PaymentBundle\Doctrine\ORM\PaymentMethodRepository as BasePaymentMethodRepository; use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\Component\Core\Repository\PaymentMethodRepositoryInterface; +/** + * @template T of PaymentMethodInterface + * + * @extends BasePaymentMethodRepository + * + * @implements PaymentMethodRepositoryInterface + */ class PaymentMethodRepository extends BasePaymentMethodRepository implements PaymentMethodRepositoryInterface { public function createListQueryBuilder(string $locale): QueryBuilder diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PaymentRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PaymentRepository.php index 81ef039313..e5f5d6e947 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PaymentRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PaymentRepository.php @@ -19,6 +19,11 @@ use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Repository\PaymentRepositoryInterface; +/** + * @template T of PaymentInterface + * + * @implements PaymentRepositoryInterface + */ class PaymentRepository extends EntityRepository implements PaymentRepositoryInterface { public function createListQueryBuilder(): QueryBuilder diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductAssociationRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductAssociationRepository.php index 9951a6ceaf..81524d6919 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductAssociationRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductAssociationRepository.php @@ -18,6 +18,11 @@ use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Repository\ProductAssociationRepositoryInterface; use Sylius\Component\Product\Model\ProductAssociationInterface; +/** + * @template T of ProductAssociationInterface + * + * @implements ProductAssociationRepositoryInterface + */ class ProductAssociationRepository extends EntityRepository implements ProductAssociationRepositoryInterface { public function findWithProductsWithinChannel($associationId, ChannelInterface $channel): ProductAssociationInterface diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductOptionRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductOptionRepository.php index fc0ce2c09e..3a935386b6 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductOptionRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductOptionRepository.php @@ -16,12 +16,17 @@ namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; use Sylius\Bundle\ProductBundle\Doctrine\ORM\ProductOptionRepository as BaseProductOptionRepository; +use Sylius\Component\Product\Model\ProductOptionInterface; use SyliusLabs\AssociationHydrator\AssociationHydrator; +/** + * @template T of ProductOptionInterface + * + * @extends BaseProductOptionRepository + */ class ProductOptionRepository extends BaseProductOptionRepository { - /** @var AssociationHydrator */ - protected $associationHydrator; + protected AssociationHydrator $associationHydrator; public function __construct(EntityManager $entityManager, ClassMetadata $class) { diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductRepository.php index 70fb7fcf31..122b689cbf 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductRepository.php @@ -23,9 +23,16 @@ use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Core\Repository\ProductRepositoryInterface; use SyliusLabs\AssociationHydrator\AssociationHydrator; +/** + * @template T of ProductInterface + * + * @extends BaseProductRepository + * + * @implements ProductRepositoryInterface + */ class ProductRepository extends BaseProductRepository implements ProductRepositoryInterface { - private AssociationHydrator $associationHydrator; + protected AssociationHydrator $associationHydrator; public function __construct(EntityManager $entityManager, ClassMetadata $class) { @@ -212,6 +219,31 @@ class ProductRepository extends BaseProductRepository implements ProductReposito ; } + public function findOneByIdHydrated(mixed $id): ?ProductInterface + { + $product = $this->createQueryBuilder('o') + ->where('o.id = :id') + ->setParameter('id', $id) + ->getQuery() + ->getOneOrNullResult() + ; + + $this->associationHydrator->hydrateAssociations($product, [ + 'images', + 'options', + 'options.translations', + 'variants', + 'variants.channelPricings', + 'variants.optionValues', + 'variants.optionValues.translations', + 'attributes', + 'attributes.attribute', + 'attributes.attribute.translations', + ]); + + return $product; + } + public function findByTaxon(TaxonInterface $taxon): array { return $this @@ -225,4 +257,42 @@ class ProductRepository extends BaseProductRepository implements ProductReposito ->getResult() ; } + + public function findOneByChannelAndCodeWithAvailableAssociations(ChannelInterface $channel, string $code): ?ProductInterface + { + $product = $this->createQueryBuilder('o') + ->addSelect('association') + ->leftJoin('o.associations', 'association') + ->innerJoin('association.associatedProducts', 'associatedProduct', 'WITH', 'associatedProduct.enabled = :enabled') + ->where('o.code = :code') + ->andWhere(':channel MEMBER OF o.channels') + ->andWhere('o.enabled = :enabled') + ->setParameter('channel', $channel) + ->setParameter('code', $code) + ->setParameter('enabled', true) + ->getQuery() + ->getOneOrNullResult() + ; + + if (null === $product) { + $product = $this->findOneByChannelAndCode($channel, $code); + if (null === $product) { + return null; + } + + $product->getAssociations()->clear(); + } + + $this->associationHydrator->hydrateAssociations($product, [ + 'images', + 'options', + 'options.translations', + 'variants', + 'variants.channelPricings', + 'variants.optionValues', + 'variants.optionValues.translations', + ]); + + return $product; + } } diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php index 39c7736edf..903073c960 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductReviewRepository.php @@ -19,6 +19,11 @@ use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Repository\ProductReviewRepositoryInterface; use Sylius\Component\Review\Model\ReviewInterface; +/** + * @template T of ReviewInterface + * + * @implements ProductReviewRepositoryInterface + */ class ProductReviewRepository extends EntityRepository implements ProductReviewRepositoryInterface { public function findLatestByProductId($productId, int $count): array diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductTaxonRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductTaxonRepository.php index 0340f2a911..405b4c4dcf 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductTaxonRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductTaxonRepository.php @@ -17,6 +17,11 @@ use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Core\Model\ProductTaxonInterface; use Sylius\Component\Core\Repository\ProductTaxonRepositoryInterface; +/** + * @template T of ProductTaxonInterface + * + * @implements ProductTaxonRepositoryInterface + */ class ProductTaxonRepository extends EntityRepository implements ProductTaxonRepositoryInterface { public function findOneByProductCodeAndTaxonCode(string $productCode, string $taxonCode): ?ProductTaxonInterface diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductVariantRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductVariantRepository.php index 02e62b51ac..cc991a0e80 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductVariantRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ProductVariantRepository.php @@ -16,9 +16,17 @@ namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ProductBundle\Doctrine\ORM\ProductVariantRepository as BaseProductVariantRepository; use Sylius\Component\Core\Model\CatalogPromotionInterface; +use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Core\Repository\ProductVariantRepositoryInterface; +/** + * @template T of ProductVariantInterface + * + * @extends BaseProductVariantRepository + * + * @implements ProductVariantRepositoryInterface + */ class ProductVariantRepository extends BaseProductVariantRepository implements ProductVariantRepositoryInterface { public function createInventoryListQueryBuilder(string $locale): QueryBuilder diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PromotionRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PromotionRepository.php index f472b870ff..c3ada88bc2 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PromotionRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/PromotionRepository.php @@ -17,9 +17,17 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadata; use Sylius\Bundle\PromotionBundle\Doctrine\ORM\PromotionRepository as BasePromotionRepository; use Sylius\Component\Channel\Model\ChannelInterface; +use Sylius\Component\Core\Model\PromotionInterface; use Sylius\Component\Core\Repository\PromotionRepositoryInterface; use SyliusLabs\AssociationHydrator\AssociationHydrator; +/** + * @template T of PromotionInterface + * + * @extends BasePromotionRepository + * + * @implements PromotionRepositoryInterface + */ class PromotionRepository extends BasePromotionRepository implements PromotionRepositoryInterface { private AssociationHydrator $associationHydrator; diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShipmentRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShipmentRepository.php index 938082cb25..50e6569604 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShipmentRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShipmentRepository.php @@ -20,6 +20,11 @@ use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ShipmentInterface; use Sylius\Component\Core\Repository\ShipmentRepositoryInterface; +/** + * @template T of ShipmentInterface + * + * @implements ShipmentRepositoryInterface + */ class ShipmentRepository extends EntityRepository implements ShipmentRepositoryInterface { public function createListQueryBuilder(): QueryBuilder diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShippingCategoryRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShippingCategoryRepository.php index 14d254de02..7af1b65926 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShippingCategoryRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShippingCategoryRepository.php @@ -16,7 +16,13 @@ namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Core\Repository\ShippingCategoryRepositoryInterface; +use Sylius\Component\Shipping\Model\ShippingCategoryInterface; +/** + * @template T of ShippingCategoryInterface + * + * @implements ShippingCategoryRepositoryInterface + */ class ShippingCategoryRepository extends EntityRepository implements ShippingCategoryRepositoryInterface { public function createListQueryBuilder(): QueryBuilder diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShippingMethodRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShippingMethodRepository.php index d1768d8c95..f7a0af2615 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShippingMethodRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/ShippingMethodRepository.php @@ -16,8 +16,16 @@ namespace Sylius\Bundle\CoreBundle\Doctrine\ORM; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ShippingBundle\Doctrine\ORM\ShippingMethodRepository as BaseShippingMethodRepository; use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Model\ShippingMethodInterface; use Sylius\Component\Core\Repository\ShippingMethodRepositoryInterface; +/** + * @template T of ShippingMethodInterface + * + * @extends BaseShippingMethodRepository + * + * @implements ShippingMethodRepositoryInterface + */ class ShippingMethodRepository extends BaseShippingMethodRepository implements ShippingMethodRepositoryInterface { public function createListQueryBuilder(string $locale): QueryBuilder diff --git a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/UserRepository.php b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/UserRepository.php index 453e02c020..6dcae8ee66 100644 --- a/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/UserRepository.php +++ b/src/Sylius/Bundle/CoreBundle/Doctrine/ORM/UserRepository.php @@ -17,6 +17,13 @@ use Sylius\Bundle\UserBundle\Doctrine\ORM\UserRepository as BaseUserRepository; use Sylius\Component\User\Model\UserInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; +/** + * @template T of UserInterface + * + * @extends BaseUserRepository + * + * @implements UserRepositoryInterface + */ class UserRepository extends BaseUserRepository implements UserRepositoryInterface { public function findOneByEmail(string $email): ?UserInterface diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/ChannelDeletionListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/ChannelDeletionListener.php index 742375b63c..9966020af5 100644 --- a/src/Sylius/Bundle/CoreBundle/EventListener/ChannelDeletionListener.php +++ b/src/Sylius/Bundle/CoreBundle/EventListener/ChannelDeletionListener.php @@ -14,13 +14,13 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\EventListener; use Sylius\Bundle\ResourceBundle\Event\ResourceControllerEvent; +use Sylius\Component\Channel\Checker\ChannelDeletionCheckerInterface; use Sylius\Component\Channel\Model\ChannelInterface; -use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; use Sylius\Component\Resource\Exception\UnexpectedTypeException; final class ChannelDeletionListener { - public function __construct(private ChannelRepositoryInterface $channelRepository) + public function __construct(private ChannelDeletionCheckerInterface $channelDeletionChecker) { } @@ -38,9 +38,7 @@ final class ChannelDeletionListener ); } - $results = $this->channelRepository->findBy(['enabled' => true]); - - if (!$results || (count($results) === 1 && current($results) === $channel)) { + if (!$this->channelDeletionChecker->isDeletable($channel)) { $event->stop('sylius.channel.delete_error'); } } diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/MailerListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/MailerListener.php index f769c39201..3598cae63d 100644 --- a/src/Sylius/Bundle/CoreBundle/EventListener/MailerListener.php +++ b/src/Sylius/Bundle/CoreBundle/EventListener/MailerListener.php @@ -16,6 +16,7 @@ namespace Sylius\Bundle\CoreBundle\EventListener; use Sylius\Bundle\CoreBundle\Mailer\Emails as CoreBundleEmails; use Sylius\Bundle\UserBundle\Mailer\Emails as UserBundleEmails; use Sylius\Component\Channel\Context\ChannelContextInterface; +use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Locale\Context\LocaleContextInterface; @@ -48,8 +49,24 @@ final class MailerListener $this->sendEmail($event->getSubject(), UserBundleEmails::EMAIL_VERIFICATION_TOKEN); } + public function sendVerificationSuccessEmail(GenericEvent $event): void + { + $user = $event->getSubject(); + + Assert::isInstanceOf($user, ShopUserInterface::class); + + $this->sendEmail($user, CoreBundleEmails::USER_REGISTRATION); + } + public function sendUserRegistrationEmail(GenericEvent $event): void { + $channel = $this->channelContext->getChannel(); + Assert::isInstanceOf($channel, ChannelInterface::class); + + if ($channel->isAccountVerificationRequired()) { + return; + } + $customer = $event->getSubject(); Assert::isInstanceOf($customer, CustomerInterface::class); diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/PostgreSQLDefaultSchemaListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/PostgreSQLDefaultSchemaListener.php new file mode 100644 index 0000000000..440ae386b3 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/PostgreSQLDefaultSchemaListener.php @@ -0,0 +1,37 @@ +getEntityManager()->getConnection()->createSchemaManager(); + + if (!is_a($schemaManager, PostgreSQLSchemaManager::class)) { + return; + } + + $schema = $args->getSchema(); + + foreach ($schemaManager->listSchemaNames() as $namespace) { + if (!$schema->hasNamespace($namespace)) { + $schema->createNamespace($namespace); + } + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/TaxonDeletionListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/TaxonDeletionListener.php index 4644fd4fa9..20f7e3524a 100644 --- a/src/Sylius/Bundle/CoreBundle/EventListener/TaxonDeletionListener.php +++ b/src/Sylius/Bundle/CoreBundle/EventListener/TaxonDeletionListener.php @@ -31,7 +31,7 @@ final class TaxonDeletionListener private array $ruleUpdaters; public function __construct( - private SessionInterface|RequestStack $requestStackOrSession, + private RequestStack|SessionInterface $requestStackOrSession, private ChannelRepositoryInterface $channelRepository, private TaxonInPromotionRuleCheckerInterface $taxonInPromotionRuleChecker, TaxonAwareRuleUpdaterInterface ...$ruleUpdaters, @@ -39,7 +39,14 @@ final class TaxonDeletionListener $this->ruleUpdaters = $ruleUpdaters; if ($requestStackOrSession instanceof SessionInterface) { - trigger_deprecation('sylius/user-bundle', '1.12', sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', SessionInterface::class, self::class, RequestStack::class)); + trigger_deprecation( + 'sylius/user-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + SessionInterface::class, + self::class, + RequestStack::class, + ); } } diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderNumberListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderNumberListener.php new file mode 100644 index 0000000000..b462751b25 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderNumberListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderNumberAssigner->assignNumber($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderTokenListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderTokenListener.php new file mode 100644 index 0000000000..33216edd71 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/AssignOrderTokenListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderTokenAssigner->assignTokenValueIfNotSet($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderPaymentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderPaymentListener.php new file mode 100644 index 0000000000..93086f32fc --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderPaymentListener.php @@ -0,0 +1,38 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + if ($this->compositeOrderStateMachine->can($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_CANCEL)) { + $this->compositeOrderStateMachine->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_CANCEL); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderShippingListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderShippingListener.php new file mode 100644 index 0000000000..54702ef4c2 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelOrderShippingListener.php @@ -0,0 +1,38 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + if ($this->compositeOrderStateMachine->can($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_CANCEL)) { + $this->compositeOrderStateMachine->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_CANCEL); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelPaymentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelPaymentListener.php new file mode 100644 index 0000000000..166853720d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelPaymentListener.php @@ -0,0 +1,42 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $payments = $order->getPayments(); + + foreach ($payments as $payment) { + if ($this->compositeOrderStateMachine->can($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL)) { + $this->compositeOrderStateMachine->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL); + } + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelShipmentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelShipmentListener.php new file mode 100644 index 0000000000..af6d57b0f1 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CancelShipmentListener.php @@ -0,0 +1,42 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $shipments = $order->getShipments(); + + foreach ($shipments as $shipment) { + if ($this->compositeOrderStateMachine->can($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL)) { + $this->compositeOrderStateMachine->apply($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL); + } + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreatePaymentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreatePaymentListener.php new file mode 100644 index 0000000000..fbdb46600c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreatePaymentListener.php @@ -0,0 +1,42 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $payments = $order->getPayments(); + + foreach ($payments as $payment) { + if ($this->compositeStateMachine->can($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CREATE)) { + $this->compositeStateMachine->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CREATE); + } + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreateShipmentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreateShipmentListener.php new file mode 100644 index 0000000000..073bc3679e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/CreateShipmentListener.php @@ -0,0 +1,42 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $shipments = $order->getShipments(); + + foreach ($shipments as $shipment) { + if ($this->compositeStateMachine->can($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CREATE)) { + $this->compositeStateMachine->apply($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CREATE); + } + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/DecrementPromotionUsagesListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/DecrementPromotionUsagesListener.php new file mode 100644 index 0000000000..2c945743b4 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/DecrementPromotionUsagesListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderPromotionsUsageModifier->decrement($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/GiveBackInventoryListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/GiveBackInventoryListener.php new file mode 100644 index 0000000000..96899a21a0 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/GiveBackInventoryListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderInventoryOperator->cancel($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/HoldInventoryListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/HoldInventoryListener.php new file mode 100644 index 0000000000..025b28c725 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/HoldInventoryListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderInventoryOperator->hold($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/IncrementPromotionUsagesListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/IncrementPromotionUsagesListener.php new file mode 100644 index 0000000000..0cf3c6af33 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/IncrementPromotionUsagesListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderPromotionsUsageModifier->increment($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderPaymentListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderPaymentListener.php new file mode 100644 index 0000000000..a35098c472 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderPaymentListener.php @@ -0,0 +1,38 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + if ($this->compositeStateMachine->can($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_REQUEST_PAYMENT)) { + $this->compositeStateMachine->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_REQUEST_PAYMENT); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderShippingListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderShippingListener.php new file mode 100644 index 0000000000..cea3426e7b --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/RequestOrderShippingListener.php @@ -0,0 +1,38 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + if ($this->compositeStateMachine->can($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_REQUEST_SHIPPING)) { + $this->compositeStateMachine->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_REQUEST_SHIPPING); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SaveCustomerAddressesListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SaveCustomerAddressesListener.php new file mode 100644 index 0000000000..6843f98612 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SaveCustomerAddressesListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderAddressesSaver->saveAddresses($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SetImmutableNamesListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SetImmutableNamesListener.php new file mode 100644 index 0000000000..5d94c2122e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Order/SetImmutableNamesListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderItemNamesSetter->__invoke($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ApplyCreateTransitionOnOrderListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ApplyCreateTransitionOnOrderListener.php new file mode 100644 index 0000000000..68602dc9db --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ApplyCreateTransitionOnOrderListener.php @@ -0,0 +1,38 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + if ($this->compositeStateMachine->can($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CREATE)) { + $this->compositeStateMachine->apply($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CREATE); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ProcessCartListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ProcessCartListener.php new file mode 100644 index 0000000000..a6f930df45 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ProcessCartListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderProcessor->process($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ResolveOrderCheckoutStateListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ResolveOrderCheckoutStateListener.php new file mode 100644 index 0000000000..e997ccc548 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ResolveOrderCheckoutStateListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderCheckoutStateResolver->resolve($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ResolveOrderPaymentStateListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ResolveOrderPaymentStateListener.php new file mode 100644 index 0000000000..38dc845b51 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ResolveOrderPaymentStateListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderPaymentStateResolver->resolve($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ResolveOrderShippingStateListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ResolveOrderShippingStateListener.php new file mode 100644 index 0000000000..e5bf849534 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/ResolveOrderShippingStateListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderShippingStateResolver->resolve($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/SaveCheckoutCompletionDateListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/SaveCheckoutCompletionDateListener.php new file mode 100644 index 0000000000..275cb4479d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderCheckout/SaveCheckoutCompletionDateListener.php @@ -0,0 +1,30 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $order->completeCheckout(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderPayment/ResolveOrderStateListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderPayment/ResolveOrderStateListener.php new file mode 100644 index 0000000000..a4742af378 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderPayment/ResolveOrderStateListener.php @@ -0,0 +1,34 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderStateResolver->resolve($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderPayment/SellOrderInventoryListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderPayment/SellOrderInventoryListener.php new file mode 100644 index 0000000000..833a8b8a82 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderPayment/SellOrderInventoryListener.php @@ -0,0 +1,34 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderInventoryOperator->sell($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderShipping/ResolveOrderStateListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderShipping/ResolveOrderStateListener.php new file mode 100644 index 0000000000..7a0f227660 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/OrderShipping/ResolveOrderStateListener.php @@ -0,0 +1,35 @@ +getSubject(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderStateResolver->resolve($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Payment/ProcessOrderListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Payment/ProcessOrderListener.php new file mode 100644 index 0000000000..80850e3ae6 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Payment/ProcessOrderListener.php @@ -0,0 +1,38 @@ +getSubject(); + Assert::isInstanceOf($payment, PaymentInterface::class); + + $order = $payment->getOrder(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderProcessor->process($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Payment/ResolveOrderPaymentStateListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Payment/ResolveOrderPaymentStateListener.php new file mode 100644 index 0000000000..f5ece7f42a --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Payment/ResolveOrderPaymentStateListener.php @@ -0,0 +1,38 @@ +getSubject(); + Assert::isInstanceOf($payment, PaymentInterface::class); + + $order = $payment->getOrder(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderPaymentStateResolver->resolve($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Shipment/AssignShippingDateListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Shipment/AssignShippingDateListener.php new file mode 100644 index 0000000000..ad687c83dd --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Shipment/AssignShippingDateListener.php @@ -0,0 +1,34 @@ +getSubject(); + Assert::isInstanceOf($shipment, ShipmentInterface::class); + + $this->shippingDateAssigner->assign($shipment); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Shipment/ResolveOrderShipmentStateListener.php b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Shipment/ResolveOrderShipmentStateListener.php new file mode 100644 index 0000000000..a18fcc1861 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/EventListener/Workflow/Shipment/ResolveOrderShipmentStateListener.php @@ -0,0 +1,38 @@ +getSubject(); + Assert::isInstanceOf($shipment, ShipmentInterface::class); + + $order = $shipment->getOrder(); + Assert::isInstanceOf($order, OrderInterface::class); + + $this->orderStateResolver->resolve($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Factory/OrderFactory.php b/src/Sylius/Bundle/CoreBundle/Factory/OrderFactory.php index e707496e04..1f94530c6e 100644 --- a/src/Sylius/Bundle/CoreBundle/Factory/OrderFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Factory/OrderFactory.php @@ -19,6 +19,9 @@ use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Resource\Factory\FactoryInterface; +/** + * @implements OrderFactoryInterface + */ final class OrderFactory implements OrderFactoryInterface { public function __construct( @@ -28,7 +31,10 @@ final class OrderFactory implements OrderFactoryInterface public function createNew(): OrderInterface { - return $this->decoratedFactory->createNew(); + /** @var OrderInterface $order */ + $order = $this->decoratedFactory->createNew(); + + return $order; } public function createNewCart( diff --git a/src/Sylius/Bundle/CoreBundle/Factory/OrderFactoryInterface.php b/src/Sylius/Bundle/CoreBundle/Factory/OrderFactoryInterface.php index 812deba7b0..88f9fa7ac6 100644 --- a/src/Sylius/Bundle/CoreBundle/Factory/OrderFactoryInterface.php +++ b/src/Sylius/Bundle/CoreBundle/Factory/OrderFactoryInterface.php @@ -17,7 +17,13 @@ use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Resource\Factory\FactoryInterface; +use Sylius\Component\Resource\Model\ResourceInterface; +/** + * @template T of ResourceInterface + * + * @extends FactoryInterface + */ interface OrderFactoryInterface extends FactoryInterface { public function createNewCart( diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/BookProductFixture.php b/src/Sylius/Bundle/CoreBundle/Fixture/BookProductFixture.php index 44659511ae..0e57dbc5f4 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/BookProductFixture.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/BookProductFixture.php @@ -13,7 +13,11 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Fixture; -@trigger_error('The "BookProductFixture" class is deprecated since Sylius 1.5 Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead.', \E_USER_DEPRECATED); +trigger_deprecation( + 'sylius/core-bundle', + '1.5', + 'The "BookProductFixture" class is deprecated. Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead.', +); use Faker\Factory; use Faker\Generator; diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/ChannelFixture.php b/src/Sylius/Bundle/CoreBundle/Fixture/ChannelFixture.php index 2429f71f6e..370ca42a12 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/ChannelFixture.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/ChannelFixture.php @@ -36,6 +36,7 @@ class ChannelFixture extends AbstractResourceFixture ->booleanNode('skipping_shipping_step_allowed')->end() ->booleanNode('skipping_payment_step_allowed')->end() ->booleanNode('account_verification_required')->end() + ->booleanNode('shipping_address_in_checkout_required')->end() ->scalarNode('default_locale')->cannotBeEmpty()->end() ->variableNode('locales') ->beforeNormalization() diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/AddressExampleFactory.php b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/AddressExampleFactory.php index f53183e352..fc89622233 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/AddressExampleFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/AddressExampleFactory.php @@ -139,6 +139,8 @@ class AddressExampleFactory extends AbstractExampleFactory } /** + * @param Collection $provinces + * * @throws \InvalidArgumentException */ private function getProvinceCode(Collection $provinces, string $provinceName): string diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/AdminUserExampleFactory.php b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/AdminUserExampleFactory.php index 1745c7ebb2..68cafa89f0 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/AdminUserExampleFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/AdminUserExampleFactory.php @@ -43,11 +43,21 @@ class AdminUserExampleFactory extends AbstractExampleFactory implements ExampleF $this->configureOptions($this->optionsResolver); if ($this->fileLocator === null || $this->imageUploader === null) { - @trigger_error(sprintf('Not passing a $fileLocator or/and $imageUploader to %s constructor is deprecated since Sylius 1.6 and will be removed in Sylius 2.0.', self::class), \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/core-bundle', + '1.6', + 'Not passing a $fileLocator or/and an $imageUploader to %s constructor is deprecated and will be removed in Sylius 2.0.', + self::class, + ); } if ($this->avatarImageFactory === null) { - @trigger_error(sprintf('Not passing a $avatarImageFactory to %s constructor is deprecated since Sylius 1.10 and will be removed in Sylius 2.0.', self::class), \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/core-bundle', + '1.10', + 'Not passing an $avatarImageFactory to %s constructor is deprecated and will be removed in Sylius 2.0.', + self::class, + ); } } diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ChannelExampleFactory.php b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ChannelExampleFactory.php index 1984dbd78d..0498782a97 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ChannelExampleFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ChannelExampleFactory.php @@ -51,11 +51,19 @@ class ChannelExampleFactory extends AbstractExampleFactory implements ExampleFac ?FactoryInterface $shopBillingDataFactory = null, ) { if (null === $taxonRepository) { - @trigger_error('Passing RouterInterface as the fifth argument is deprecated since 1.8 and will be prohibited in 2.0', \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/core-bundle', + '1.8', + 'Passing a $taxonRepository as the fifth argument is deprecated and will be prohibited in Sylius 2.0', + ); } if (null === $shopBillingDataFactory) { - @trigger_error('Passing RouterInterface as the sixth argument is deprecated since 1.8 and will be prohibited in 2.0', \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/core-bundle', + '1.8', + 'Passing a $shopBillingDataFactory as the sixth argument is deprecated and will be prohibited in Sylius 2.0', + ); } $this->taxonRepository = $taxonRepository; $this->shopBillingDataFactory = $shopBillingDataFactory; @@ -84,6 +92,7 @@ class ChannelExampleFactory extends AbstractExampleFactory implements ExampleFac $channel->setSkippingShippingStepAllowed($options['skipping_shipping_step_allowed']); $channel->setSkippingPaymentStepAllowed($options['skipping_payment_step_allowed']); $channel->setAccountVerificationRequired($options['account_verification_required']); + $channel->setShippingAddressInCheckoutRequired($options['shipping_address_in_checkout_required']); if (null !== $this->taxonRepository) { $channel->setMenuTaxon($options['menu_taxon']); @@ -134,6 +143,8 @@ class ChannelExampleFactory extends AbstractExampleFactory implements ExampleFac ->setAllowedTypes('skipping_payment_step_allowed', 'bool') ->setDefault('account_verification_required', true) ->setAllowedTypes('account_verification_required', 'bool') + ->setDefault('shipping_address_in_checkout_required', false) + ->setAllowedTypes('shipping_address_in_checkout_required', 'bool') ->setDefault( 'default_tax_zone', LazyOption::randomOneOrNull($this->zoneRepository, 100, ['scope' => [Scope::TAX, AddressingScope::ALL]]), diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/OrderExampleFactory.php b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/OrderExampleFactory.php index 2f70b81f1f..464a0c00fc 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/OrderExampleFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/OrderExampleFactory.php @@ -17,6 +17,8 @@ use Doctrine\Persistence\ObjectManager; use Faker\Factory; use Faker\Generator; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Bundle\CoreBundle\Fixture\OptionsResolver\LazyOption; use Sylius\Component\Addressing\Model\CountryInterface; use Sylius\Component\Core\Checker\OrderPaymentMethodSelectionRequirementCheckerInterface; @@ -61,13 +63,25 @@ class OrderExampleFactory extends AbstractExampleFactory implements ExampleFacto protected PaymentMethodRepositoryInterface $paymentMethodRepository, protected ShippingMethodRepositoryInterface $shippingMethodRepository, protected FactoryInterface $addressFactory, - protected StateMachineFactoryInterface $stateMachineFactory, + protected StateMachineFactoryInterface|StateMachineInterface $stateMachineFactory, protected OrderShippingMethodSelectionRequirementCheckerInterface $orderShippingMethodSelectionRequirementChecker, protected OrderPaymentMethodSelectionRequirementCheckerInterface $orderPaymentMethodSelectionRequirementChecker, ) { $this->optionsResolver = new OptionsResolver(); $this->faker = Factory::create(); $this->configureOptions($this->optionsResolver); + + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the twelfth argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + StateMachineFactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function create(array $options = []): OrderInterface @@ -251,7 +265,7 @@ class OrderExampleFactory extends AbstractExampleFactory implements ExampleFacto protected function applyCheckoutStateTransition(OrderInterface $order, string $transition): void { - $this->stateMachineFactory->get($order, OrderCheckoutTransitions::GRAPH)->apply($transition); + $this->getStateMachine()->apply($order, OrderCheckoutTransitions::GRAPH, $transition); } protected function generateInvalidSkipMessage(string $type, string $channelCode): string @@ -281,21 +295,32 @@ class OrderExampleFactory extends AbstractExampleFactory implements ExampleFacto protected function completePayments(OrderInterface $order): void { + $stateMachine = $this->getStateMachine(); + foreach ($order->getPayments() as $payment) { - $stateMachine = $this->stateMachineFactory->get($payment, PaymentTransitions::GRAPH); - if ($stateMachine->can(PaymentTransitions::TRANSITION_COMPLETE)) { - $stateMachine->apply(PaymentTransitions::TRANSITION_COMPLETE); + if ($stateMachine->can($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_COMPLETE)) { + $stateMachine->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_COMPLETE); } } } protected function completeShipments(OrderInterface $order): void { + $stateMachine = $this->getStateMachine(); + foreach ($order->getShipments() as $shipment) { - $stateMachine = $this->stateMachineFactory->get($shipment, ShipmentTransitions::GRAPH); - if ($stateMachine->can(ShipmentTransitions::TRANSITION_SHIP)) { - $stateMachine->apply(ShipmentTransitions::TRANSITION_SHIP); + if ($stateMachine->can($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_SHIP)) { + $stateMachine->apply($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_SHIP); } } } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } } diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductExampleFactory.php b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductExampleFactory.php index a4bd8bbca9..3f9cd862b0 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductExampleFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductExampleFactory.php @@ -66,7 +66,21 @@ class ProductExampleFactory extends AbstractExampleFactory implements ExampleFac private ?FileLocatorInterface $fileLocator = null, ) { if ($this->taxCategoryRepository === null) { - @trigger_error(sprintf('Not passing a $taxCategoryRepository to %s constructor is deprecated since Sylius 1.6 and will be removed in Sylius 2.0.', self::class), \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/core-bundle', + '1.6', + 'Not passing a $taxCategoryRepository to %s constructor is deprecated and will be prohibited in Sylius 2.0.', + self::class, + ); + } + + if ($this->fileLocator === null) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'Not passing a $fileLocator to %s constructor is deprecated and will be removed in Sylius 2.0.', + self::class, + ); } $this->faker = Factory::create(); @@ -237,10 +251,10 @@ class ProductExampleFactory extends AbstractExampleFactory implements ExampleFac { foreach ($options['images'] as $image) { if (!array_key_exists('path', $image)) { - @trigger_error( - 'It is deprecated since Sylius 1.3 to pass indexed array as an image definition. ' . - 'Please use associative array with "path" and "type" keys instead.', - \E_USER_DEPRECATED, + trigger_deprecation( + 'sylius/core-bundle', + '1.3', + 'It is deprecated to pass indexed array as an image definition. Please use associative array with "path" and "type" keys instead.', ); $imagePath = array_shift($image); diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductReviewExampleFactory.php b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductReviewExampleFactory.php index 8059595886..b892bebc5d 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductReviewExampleFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ProductReviewExampleFactory.php @@ -16,10 +16,11 @@ namespace Sylius\Bundle\CoreBundle\Fixture\Factory; use Faker\Factory; use Faker\Generator; use SM\Factory\FactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Bundle\CoreBundle\Fixture\OptionsResolver\LazyOption; use Sylius\Component\Core\Repository\CustomerRepositoryInterface; use Sylius\Component\Core\Repository\ProductRepositoryInterface; -use Sylius\Component\Resource\StateMachine\StateMachineInterface; use Sylius\Component\Review\Factory\ReviewFactoryInterface; use Sylius\Component\Review\Model\ReviewInterface; use Symfony\Component\OptionsResolver\Options; @@ -35,12 +36,24 @@ class ProductReviewExampleFactory extends AbstractExampleFactory implements Exam private ReviewFactoryInterface $productReviewFactory, private ProductRepositoryInterface $productRepository, private CustomerRepositoryInterface $customerRepository, - private FactoryInterface $stateMachineFactory, + private FactoryInterface|StateMachineInterface $stateMachineFactory, ) { $this->faker = Factory::create(); $this->optionsResolver = new OptionsResolver(); $this->configureOptions($this->optionsResolver); + + if ($this->stateMachineFactory instanceof FactoryInterface) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the fourth argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + FactoryInterface::class, + StateMachineInterface::class, + ), + ); + } } public function create(array $options = []): ReviewInterface @@ -95,12 +108,21 @@ class ProductReviewExampleFactory extends AbstractExampleFactory implements Exam private function applyReviewTransition(ReviewInterface $productReview, string $targetState): void { - /** @var StateMachineInterface $stateMachine */ - $stateMachine = $this->stateMachineFactory->get($productReview, 'sylius_product_review'); - $transition = $stateMachine->getTransitionToState($targetState); + $stateMachine = $this->getStateMachine(); + + $transition = $stateMachine->getTransitionToState($productReview, 'sylius_product_review', $targetState); if (null !== $transition) { - $stateMachine->apply($transition); + $stateMachine->apply($productReview, 'sylius_product_review', $transition); } } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof FactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } } diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionExampleFactory.php b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionExampleFactory.php index b40abbbe72..eb7a4e6fb8 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionExampleFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/PromotionExampleFactory.php @@ -20,9 +20,11 @@ use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\PromotionCouponInterface; use Sylius\Component\Core\Model\PromotionInterface; +use Sylius\Component\Locale\Model\LocaleInterface; use Sylius\Component\Promotion\Model\PromotionActionInterface; use Sylius\Component\Promotion\Model\PromotionRuleInterface; use Sylius\Component\Resource\Factory\FactoryInterface; +use Sylius\Component\Resource\Repository\RepositoryInterface; use Symfony\Component\OptionsResolver\Exception\InvalidArgumentException; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -33,12 +35,16 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF private OptionsResolver $optionsResolver; + /** + * @param RepositoryInterface|null $localeRepository + */ public function __construct( private FactoryInterface $promotionFactory, private ExampleFactoryInterface $promotionRuleExampleFactory, private ExampleFactoryInterface $promotionActionExampleFactory, private ChannelRepositoryInterface $channelRepository, private ?FactoryInterface $couponFactory = null, + private ?RepositoryInterface $localeRepository = null, ) { $this->faker = Factory::create(); $this->optionsResolver = new OptionsResolver(); @@ -46,7 +52,21 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF $this->configureOptions($this->optionsResolver); if ($this->couponFactory === null) { - @trigger_error(sprintf('Not passing a $couponFactory to %s constructor is deprecated since Sylius 1.8 and will be removed in Sylius 2.0.', self::class), \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/core-bundle', + '1.8', + 'Not passing a $couponFactory to %s constructor is deprecated and will be removed in Sylius 2.0.', + self::class, + ); + } + + if ($this->localeRepository === null) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'Not passing a $localeRepository to %s constructor is deprecated and will be prohibited in Sylius 2.0.', + self::class, + ); } } @@ -63,6 +83,7 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF $promotion->setUsageLimit($options['usage_limit']); $promotion->setExclusive($options['exclusive']); $promotion->setPriority((int) $options['priority']); + $promotion->setAppliesToDiscounted($options['applies_to_discounted']); if (isset($options['starts_at'])) { $promotion->setStartsAt(new \DateTime($options['starts_at'])); @@ -72,6 +93,10 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF $promotion->setEndsAt(new \DateTime($options['ends_at'])); } + if (isset($options['archived_at'])) { + $promotion->setArchivedAt(new \DateTime($options['archived_at'])); + } + foreach ($options['channels'] as $channel) { $promotion->addChannel($channel); } @@ -92,6 +117,13 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF $promotion->addCoupon($coupon); } + foreach ($this->getLocales() as $localeCode) { + $promotion->setCurrentLocale($localeCode); + $promotion->setFallbackLocale($localeCode); + + $promotion->setLabel($options['label']); + } + return $promotion; } @@ -100,33 +132,37 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF $resolver ->setDefault('code', fn (Options $options): string => StringInflector::nameToCode($options['name'])) ->setDefault('name', $this->faker->words(3, true)) + ->setDefault('label', fn (Options $options): string => $options['name']) ->setDefault('description', $this->faker->sentence()) ->setDefault('usage_limit', null) ->setDefault('coupon_based', false) ->setDefault('exclusive', $this->faker->boolean(25)) ->setDefault('priority', 0) + ->setDefault('applies_to_discounted', true) ->setDefault('starts_at', null) ->setAllowedTypes('starts_at', ['null', 'string']) ->setDefault('ends_at', null) ->setAllowedTypes('ends_at', ['null', 'string']) + ->setDefault('archived_at', null) + ->setAllowedTypes('archived_at', ['null', 'string']) ->setDefault('channels', LazyOption::all($this->channelRepository)) ->setAllowedTypes('channels', 'array') ->setNormalizer('channels', LazyOption::findBy($this->channelRepository, 'code')) ->setDefined('rules') - ->setNormalizer('rules', function (Options $options, array $rules): array { - if (empty($rules)) { - return [[]]; + ->setNormalizer('rules', function (Options $options, ?array $rules): array { + if (null === $rules) { + return []; } - return $rules; + return empty($rules) ? [[]] : $rules; }) ->setDefined('actions') - ->setNormalizer('actions', function (Options $options, array $actions): array { - if (empty($actions)) { - return [[]]; + ->setNormalizer('actions', function (Options $options, ?array $actions): array { + if (null === $actions) { + return []; } - return $actions; + return empty($actions) ? [[]] : $actions; }) ->setDefault('coupons', []) @@ -151,11 +187,11 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF /** @var PromotionCouponInterface $coupon */ $coupon = $couponFactory->createNew(); $coupon->setCode($couponDefinition['code']); - $coupon->setPerCustomerUsageLimit($couponDefinition['per_customer_usage_limit']); - $coupon->setReusableFromCancelledOrders($couponDefinition['reusable_from_cancelled_orders']); + $coupon->setPerCustomerUsageLimit($couponDefinition['per_customer_usage_limit'] ?? null); + $coupon->setReusableFromCancelledOrders($couponDefinition['reusable_from_cancelled_orders'] ?? true); $coupon->setUsageLimit($couponDefinition['usage_limit']); - if (null !== $couponDefinition['expires_at']) { + if (null !== ($couponDefinition['expires_at'] ?? null)) { $coupon->setExpiresAt(new \DateTime($couponDefinition['expires_at'])); } @@ -165,4 +201,18 @@ class PromotionExampleFactory extends AbstractExampleFactory implements ExampleF return $coupons; }; } + + /** @return iterable */ + private function getLocales(): iterable + { + if (null === $this->localeRepository) { + return []; + } + + /** @var LocaleInterface[] $locales */ + $locales = $this->localeRepository->findAll(); + foreach ($locales as $locale) { + yield $locale->getCode(); + } + } } diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ShippingMethodExampleFactory.php b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ShippingMethodExampleFactory.php index 968e6c32b8..d72739c202 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ShippingMethodExampleFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ShippingMethodExampleFactory.php @@ -45,7 +45,12 @@ class ShippingMethodExampleFactory extends AbstractExampleFactory implements Exa private ?RepositoryInterface $taxCategoryRepository = null, ) { if ($this->taxCategoryRepository === null) { - @trigger_error(sprintf('Not passing a $taxCategoryRepository to %s constructor is deprecated since Sylius 1.4 and will be removed in Sylius 2.0.', self::class), \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/core-bundle', + '1.4', + 'Not passing a $taxCategoryRepository to %s constructor is deprecated and will be removed in Sylius 2.0.', + self::class, + ); } $this->faker = Factory::create(); diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ShopUserExampleFactory.php b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ShopUserExampleFactory.php index df4d00be27..98fa2a283b 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ShopUserExampleFactory.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/Factory/ShopUserExampleFactory.php @@ -88,7 +88,7 @@ class ShopUserExampleFactory extends AbstractExampleFactory implements ExampleFa ->setAllowedTypes('birthday', ['null', 'string', \DateTimeInterface::class]) ->setNormalizer( 'birthday', - function (Options $options, string|\DateTimeInterface|null $value) { + function (Options $options, \DateTimeInterface|string|null $value) { if (is_string($value)) { return \DateTime::createFromFormat('Y-m-d H:i:s', $value); } diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/MugProductFixture.php b/src/Sylius/Bundle/CoreBundle/Fixture/MugProductFixture.php index 865073487f..08f0129969 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/MugProductFixture.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/MugProductFixture.php @@ -16,13 +16,20 @@ namespace Sylius\Bundle\CoreBundle\Fixture; use Faker\Factory; use Faker\Generator; -@trigger_error('The "MugProductFixture" class is deprecated since Sylius 1.5 Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead.', \E_USER_DEPRECATED); +trigger_deprecation( + 'sylius/core-bundle', + '1.5', + 'The "MugProductFixture" class is deprecated and will be removed in Sylius 2.0. Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead.', +); use Sylius\Bundle\FixturesBundle\Fixture\AbstractFixture; use Sylius\Component\Attribute\AttributeType\SelectAttributeType; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @deprecated since Sylius 1.5 and will be removed in Sylius 2.0. Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead. + */ class MugProductFixture extends AbstractFixture { private Generator $faker; diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/OrderFixture.php b/src/Sylius/Bundle/CoreBundle/Fixture/OrderFixture.php index a265782f5d..c7d1e906ec 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/OrderFixture.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/OrderFixture.php @@ -17,6 +17,7 @@ use Doctrine\Persistence\ObjectManager; use Faker\Factory; use Faker\Generator; use SM\Factory\FactoryInterface as StateMachineFactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\CoreBundle\Fixture\Factory\OrderExampleFactory; use Sylius\Bundle\FixturesBundle\Fixture\AbstractFixture; use Sylius\Component\Core\Checker\OrderPaymentMethodSelectionRequirementCheckerInterface; @@ -52,7 +53,7 @@ class OrderFixture extends AbstractFixture PaymentMethodRepositoryInterface $paymentMethodRepository, ShippingMethodRepositoryInterface $shippingMethodRepository, FactoryInterface $addressFactory, - StateMachineFactoryInterface $stateMachineFactory, + StateMachineFactoryInterface|StateMachineInterface $stateMachineFactory, OrderShippingMethodSelectionRequirementCheckerInterface $orderShippingMethodSelectionRequirementChecker, OrderPaymentMethodSelectionRequirementCheckerInterface $orderPaymentMethodSelectionRequirementChecker, OrderExampleFactory $orderExampleFactory = null, @@ -77,7 +78,11 @@ class OrderFixture extends AbstractFixture $orderPaymentMethodSelectionRequirementChecker, ); - @trigger_error('Use orderExampleFactory. OrderFixture is deprecated since 1.6 and will be prohibited since 2.0.', \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/core-bundle', + '1.6', + 'Use OrderExampleFactory. OrderFixture is deprecated and will be prohibited since Sylius 2.0.', + ); } $this->orderManager = $orderManager; diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/PaymentFixture.php b/src/Sylius/Bundle/CoreBundle/Fixture/PaymentFixture.php new file mode 100644 index 0000000000..8eee166c32 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Fixture/PaymentFixture.php @@ -0,0 +1,126 @@ + $paymentRepository + */ + public function __construct( + private PaymentRepositoryInterface $paymentRepository, + private StateMachineFactoryInterface|StateMachineInterface $stateMachineFactory, + private ObjectManager $paymentManager, + ) { + $this->faker = Factory::create(); + + $this->optionsResolver = new OptionsResolver(); + $this->configureOptions($this->optionsResolver); + + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the second argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + StateMachineFactoryInterface::class, + StateMachineInterface::class, + ), + ); + } + } + + public function getName(): string + { + return 'payments'; + } + + /** + * @param string[] $options + */ + public function load(array $options): void + { + $options = $this->optionsResolver->resolve($options); + + $payments = $this->paymentRepository->findAll(); + + /** @var PaymentInterface $payment */ + foreach ($payments as $payment) { + if ($this->faker->boolean($options['percentage_completed'])) { + $this->completePayment($payment); + } + } + + $this->paymentManager->flush(); + } + + protected function configureOptionsNode(ArrayNodeDefinition $optionsNode): void + { + $optionsNode + ->children() + ->integerNode('percentage_completed')->isRequired()->min(0)->max(100)->end() + ; + } + + private function completePayment(PaymentInterface $payment): void + { + $this->getStateMachine()->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_COMPLETE); + + $this->paymentManager->persist($payment); + } + + private function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('percentage_completed', 0) + ->setAllowedTypes('percentage_completed', 'int') + ; + } + + private function getStateMachine(): StateMachineInterface + { + if ($this->stateMachineFactory instanceof StateMachineFactoryInterface) { + return new WinzouStateMachineAdapter($this->stateMachineFactory); + } + + return $this->stateMachineFactory; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/PromotionFixture.php b/src/Sylius/Bundle/CoreBundle/Fixture/PromotionFixture.php index 87b79cdb15..ede34631e9 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/PromotionFixture.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/PromotionFixture.php @@ -40,6 +40,7 @@ class PromotionFixture extends AbstractResourceFixture ->end() ->scalarNode('starts_at')->cannotBeEmpty()->end() ->scalarNode('ends_at')->cannotBeEmpty()->end() + ->scalarNode('archived_at')->defaultNull()->end() ->arrayNode('rules') ->requiresAtLeastOneElement() ->arrayPrototype() diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/StickerProductFixture.php b/src/Sylius/Bundle/CoreBundle/Fixture/StickerProductFixture.php index 91c232c39d..76217d9288 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/StickerProductFixture.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/StickerProductFixture.php @@ -13,7 +13,11 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Fixture; -@trigger_error('The "StickerProductFixture" class is deprecated since Sylius 1.5 Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead.', \E_USER_DEPRECATED); +trigger_deprecation( + 'sylius/core-bundle', + '1.5', + 'The "StickerProductFixture" class is deprecated. Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead.', +); use Faker\Factory; use Faker\Generator; @@ -23,6 +27,9 @@ use Sylius\Component\Core\Model\ProductInterface; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @deprecated since Sylius 1.5 and will be removed in Sylius 2.0. Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead. + */ class StickerProductFixture extends AbstractFixture { private Generator $faker; diff --git a/src/Sylius/Bundle/CoreBundle/Fixture/TshirtProductFixture.php b/src/Sylius/Bundle/CoreBundle/Fixture/TshirtProductFixture.php index dcb8abc989..d91f7dac7a 100644 --- a/src/Sylius/Bundle/CoreBundle/Fixture/TshirtProductFixture.php +++ b/src/Sylius/Bundle/CoreBundle/Fixture/TshirtProductFixture.php @@ -16,13 +16,20 @@ namespace Sylius\Bundle\CoreBundle\Fixture; use Faker\Factory; use Faker\Generator; -@trigger_error('The "TshirtProductFixture" class is deprecated since Sylius 1.5 Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead.', \E_USER_DEPRECATED); +trigger_deprecation( + 'sylius/core-bundle', + '1.5', + 'The "TshirtProductFixture" class is deprecated. Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead.', +); use Sylius\Bundle\FixturesBundle\Fixture\AbstractFixture; use Sylius\Component\Attribute\AttributeType\TextAttributeType; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @deprecated since Sylius 1.5 and will be removed in Sylius 2.0. Use new product fixtures class located at "src/Sylius/Bundle/CoreBundle/Fixture/" instead. + */ class TshirtProductFixture extends AbstractFixture { private Generator $faker; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/CartItemTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/CartItemTypeExtension.php index b596d35fb5..efec1a0a3e 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/CartItemTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/CartItemTypeExtension.php @@ -66,11 +66,6 @@ final class CartItemTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string - { - return CartItemType::class; - } - public static function getExtendedTypes(): iterable { return [CartItemType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/CartTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/CartTypeExtension.php index b83ccbb6c3..1d68cfffb0 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/CartTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/CartTypeExtension.php @@ -48,11 +48,6 @@ final class CartTypeExtension extends AbstractTypeExtension }); } - public function getExtendedType(): string - { - return CartType::class; - } - public static function getExtendedTypes(): iterable { return [CartType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/CatalogPromotionTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/CatalogPromotionTypeExtension.php index 0c096ef615..e6963bae06 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/CatalogPromotionTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/CatalogPromotionTypeExtension.php @@ -32,11 +32,6 @@ final class CatalogPromotionTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string - { - return CatalogPromotionType::class; - } - public static function getExtendedTypes(): iterable { return [CatalogPromotionType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/ChannelTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/ChannelTypeExtension.php index a2b9313f59..5ea7e076e5 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/ChannelTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/ChannelTypeExtension.php @@ -18,6 +18,7 @@ use Sylius\Bundle\AddressingBundle\Form\Type\ZoneChoiceType; use Sylius\Bundle\ChannelBundle\Form\Type\ChannelType; use Sylius\Bundle\CoreBundle\Form\EventSubscriber\AddBaseCurrencySubscriber; use Sylius\Bundle\CoreBundle\Form\EventSubscriber\ChannelFormSubscriber; +use Sylius\Bundle\CoreBundle\Form\Type\ChannelPriceHistoryConfigType; use Sylius\Bundle\CoreBundle\Form\Type\ShopBillingDataType; use Sylius\Bundle\CoreBundle\Form\Type\TaxCalculationStrategyChoiceType; use Sylius\Bundle\CurrencyBundle\Form\Type\CurrencyChoiceType; @@ -102,16 +103,15 @@ final class ChannelTypeExtension extends AbstractTypeExtension ->add('menuTaxon', TaxonAutocompleteChoiceType::class, [ 'label' => 'sylius.form.channel.menu_taxon', ]) + ->add('channelPriceHistoryConfig', ChannelPriceHistoryConfigType::class, [ + 'label' => false, + 'required' => false, + ]) ->addEventSubscriber(new AddBaseCurrencySubscriber()) ->addEventSubscriber(new ChannelFormSubscriber()) ; } - public function getExtendedType(): string - { - return ChannelType::class; - } - public static function getExtendedTypes(): iterable { return [ChannelType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/CountryTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/CountryTypeExtension.php index 8aa12aeeed..7784720f6f 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/CountryTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/CountryTypeExtension.php @@ -65,11 +65,6 @@ final class CountryTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string - { - return CountryType::class; - } - public static function getExtendedTypes(): iterable { return [CountryType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/CustomerTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/CustomerTypeExtension.php index 3a54006e86..12a15f1e3a 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/CustomerTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/CustomerTypeExtension.php @@ -26,11 +26,6 @@ final class CustomerTypeExtension extends AbstractTypeExtension $builder->addEventSubscriber(new AddUserFormSubscriber(ShopUserType::class)); } - public function getExtendedType(): string - { - return CustomerType::class; - } - public static function getExtendedTypes(): iterable { return [CustomerType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/LocaleTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/LocaleTypeExtension.php index d11962c302..de801de86a 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/LocaleTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/LocaleTypeExtension.php @@ -50,11 +50,6 @@ final class LocaleTypeExtension extends AbstractTypeExtension }); } - public function getExtendedType(): string - { - return LocaleType::class; - } - public static function getExtendedTypes(): iterable { return [LocaleType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/OrderTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/OrderTypeExtension.php index a70a92d9df..acb7f5777b 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/OrderTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/OrderTypeExtension.php @@ -28,11 +28,6 @@ final class OrderTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string - { - return OrderType::class; - } - public static function getExtendedTypes(): iterable { return [OrderType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/PaymentMethodTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/PaymentMethodTypeExtension.php index 677843e2fc..021552da59 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/PaymentMethodTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/PaymentMethodTypeExtension.php @@ -16,15 +16,21 @@ namespace Sylius\Bundle\CoreBundle\Form\Extension; use Sylius\Bundle\ChannelBundle\Form\Type\ChannelChoiceType; use Sylius\Bundle\PaymentBundle\Form\Type\PaymentMethodType; use Sylius\Bundle\PayumBundle\Form\Type\GatewayConfigType; +use Sylius\Bundle\PayumBundle\Validator\GroupsGenerator\GatewayConfigGroupsGenerator; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\PaymentMethodInterface; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; +use Symfony\Component\OptionsResolver\OptionsResolver; final class PaymentMethodTypeExtension extends AbstractTypeExtension { + public function __construct(private GatewayConfigGroupsGenerator $gatewayConfigGroupsGenerator) + { + } + public function buildForm(FormBuilderInterface $builder, array $options): void { $gatewayFactory = $options['data']->getGatewayConfig(); @@ -57,9 +63,11 @@ final class PaymentMethodTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string + public function configureOptions(OptionsResolver $resolver): void { - return PaymentMethodType::class; + $resolver->setDefaults([ + 'validation_groups' => $this->gatewayConfigGroupsGenerator, + ]); } public static function getExtendedTypes(): iterable diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductTranslationTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductTranslationTypeExtension.php index 6f795d85cd..a4046520f3 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductTranslationTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductTranslationTypeExtension.php @@ -30,11 +30,6 @@ final class ProductTranslationTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string - { - return ProductTranslationType::class; - } - public static function getExtendedTypes(): iterable { return [ProductTranslationType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductTypeExtension.php index 6d246578ab..d1b91dea83 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductTypeExtension.php @@ -65,11 +65,6 @@ final class ProductTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string - { - return ProductType::class; - } - public static function getExtendedTypes(): iterable { return [ProductType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductVariantGenerationTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductVariantGenerationTypeExtension.php index 4381d710a7..bb4731f030 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductVariantGenerationTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductVariantGenerationTypeExtension.php @@ -40,11 +40,6 @@ final class ProductVariantGenerationTypeExtension extends AbstractTypeExtension }); } - public function getExtendedType(): string - { - return ProductVariantGenerationType::class; - } - public static function getExtendedTypes(): iterable { return [ProductVariantGenerationType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductVariantTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductVariantTypeExtension.php index d3b6be28fa..ff626a98f6 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductVariantTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/ProductVariantTypeExtension.php @@ -91,11 +91,6 @@ final class ProductVariantTypeExtension extends AbstractTypeExtension }); } - public function getExtendedType(): string - { - return ProductVariantType::class; - } - public static function getExtendedTypes(): iterable { return [ProductVariantType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionCouponTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionCouponTypeExtension.php index 619ff13e8d..ebcfce7b81 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionCouponTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionCouponTypeExtension.php @@ -35,11 +35,6 @@ final class PromotionCouponTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string - { - return PromotionCouponType::class; - } - public static function getExtendedTypes(): iterable { return [PromotionCouponType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionFilterCollectionTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionFilterCollectionTypeExtension.php index 2a7b9b9327..1ea3c9b514 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionFilterCollectionTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionFilterCollectionTypeExtension.php @@ -33,11 +33,6 @@ final class PromotionFilterCollectionTypeExtension extends AbstractTypeExtension ]); } - public function getExtendedType(): string - { - return PromotionFilterCollectionType::class; - } - public static function getExtendedTypes(): iterable { return [PromotionFilterCollectionType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionTypeExtension.php index b149ee4fe9..f745e1d34c 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/PromotionTypeExtension.php @@ -31,11 +31,6 @@ final class PromotionTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string - { - return PromotionType::class; - } - public static function getExtendedTypes(): iterable { return [PromotionType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/ShippingMethodTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/ShippingMethodTypeExtension.php index 7d3044fc99..db50227eed 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/ShippingMethodTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/ShippingMethodTypeExtension.php @@ -16,13 +16,19 @@ namespace Sylius\Bundle\CoreBundle\Form\Extension; use Sylius\Bundle\AddressingBundle\Form\Type\ZoneChoiceType; use Sylius\Bundle\ChannelBundle\Form\Type\ChannelChoiceType; use Sylius\Bundle\ShippingBundle\Form\Type\ShippingMethodType; +use Sylius\Bundle\ShippingBundle\Validator\GroupsGenerator\ShippingMethodConfigurationGroupsGenerator; use Sylius\Bundle\TaxationBundle\Form\Type\TaxCategoryChoiceType; use Sylius\Component\Core\Model\Scope; use Symfony\Component\Form\AbstractTypeExtension; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; final class ShippingMethodTypeExtension extends AbstractTypeExtension { + public function __construct(private ShippingMethodConfigurationGroupsGenerator $configurationGroupsGenerator) + { + } + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder @@ -43,9 +49,11 @@ final class ShippingMethodTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string + public function configureOptions(OptionsResolver $resolver): void { - return ShippingMethodType::class; + $resolver->setDefaults([ + 'validation_groups' => $this->configurationGroupsGenerator, + ]); } public static function getExtendedTypes(): iterable diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/TaxRateTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/TaxRateTypeExtension.php index 5582528e0b..265f498f60 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/TaxRateTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/TaxRateTypeExtension.php @@ -26,11 +26,6 @@ final class TaxRateTypeExtension extends AbstractTypeExtension $builder->add('zone', ZoneChoiceType::class, ['zone_scope' => Scope::TAX]); } - public function getExtendedType(): string - { - return TaxRateType::class; - } - public static function getExtendedTypes(): iterable { return [TaxRateType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Extension/TaxonTypeExtension.php b/src/Sylius/Bundle/CoreBundle/Form/Extension/TaxonTypeExtension.php index 66c6dfb7bb..51fb9c8a14 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Extension/TaxonTypeExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Extension/TaxonTypeExtension.php @@ -34,11 +34,6 @@ final class TaxonTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string - { - return TaxonType::class; - } - public static function getExtendedTypes(): iterable { return [TaxonType::class]; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForProductsScopeConfigurationType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForProductsScopeConfigurationType.php index 60c1571366..a0a6596597 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForProductsScopeConfigurationType.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForProductsScopeConfigurationType.php @@ -17,7 +17,6 @@ use Sylius\Bundle\ProductBundle\Form\Type\ProductAutocompleteChoiceType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; final class ForProductsScopeConfigurationType extends AbstractType { @@ -34,9 +33,6 @@ final class ForProductsScopeConfigurationType extends AbstractType 'choice_name' => 'name', 'choice_value' => 'code', 'resource' => 'sylius.product', - 'constraints' => [ - new NotBlank(['groups' => 'sylius', 'message' => 'sylius.catalog_promotion_scope.for_products.not_empty']), - ], ]); $builder->get('products')->addModelTransformer($this->productsToCodesTransformer); diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForTaxonsScopeConfigurationType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForTaxonsScopeConfigurationType.php index 719486de73..0624af22ea 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForTaxonsScopeConfigurationType.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForTaxonsScopeConfigurationType.php @@ -17,7 +17,6 @@ use Sylius\Bundle\TaxonomyBundle\Form\Type\TaxonAutocompleteChoiceType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; final class ForTaxonsScopeConfigurationType extends AbstractType { @@ -33,9 +32,6 @@ final class ForTaxonsScopeConfigurationType extends AbstractType 'required' => false, 'choice_value' => 'code', 'resource' => 'sylius.taxon', - 'constraints' => [ - new NotBlank(['groups' => 'sylius', 'message' => 'sylius.catalog_promotion_scope.for_taxons.not_empty']), - ], ]); $builder->get('taxons')->addModelTransformer($this->taxonsToCodesTransformer); diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForVariantsScopeConfigurationType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForVariantsScopeConfigurationType.php index f2e175d014..cfacee1828 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForVariantsScopeConfigurationType.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/CatalogPromotionScope/ForVariantsScopeConfigurationType.php @@ -17,7 +17,6 @@ use Sylius\Bundle\ResourceBundle\Form\Type\ResourceAutocompleteChoiceType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\DataTransformerInterface; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; final class ForVariantsScopeConfigurationType extends AbstractType { @@ -34,9 +33,6 @@ final class ForVariantsScopeConfigurationType extends AbstractType 'choice_name' => 'descriptor', 'choice_value' => 'code', 'resource' => 'sylius.product_variant', - 'constraints' => [ - new NotBlank(['groups' => 'sylius', 'message' => 'sylius.catalog_promotion_scope.for_variants.not_empty']), - ], ]); $builder->get('variants')->addModelTransformer($this->productVariantsToCodesTransformer); diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/ChannelPriceHistoryConfigType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/ChannelPriceHistoryConfigType.php new file mode 100644 index 0000000000..fb75976b2a --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/ChannelPriceHistoryConfigType.php @@ -0,0 +1,90 @@ +add('lowestPriceForDiscountedProductsVisible', CheckboxType::class, [ + 'label' => 'sylius.form.admin.channel.lowest_price_for_discounted_products_visible', + 'required' => false, + ]) + ->add('lowestPriceForDiscountedProductsCheckingPeriod', IntegerType::class, [ + 'label' => 'sylius.form.admin.channel.period_for_which_the_lowest_price_is_calculated', + ]) + ->add('taxonsExcludedFromShowingLowestPrice', TaxonAutocompleteChoiceType::class, [ + 'label' => 'sylius.ui.taxons_for_which_the_lowest_price_is_not_displayed', + 'required' => false, + 'multiple' => true, + ]) + ; + + $builder->setDataMapper($this); + } + + public function mapDataToForms(mixed $viewData, \Traversable $forms): void + { + $this->propertyPathDataMapper->mapDataToForms($viewData, $forms); + } + + public function mapFormsToData(\Traversable $forms, mixed &$viewData): void + { + Assert::isInstanceOf($channelPriceHistoryConfig = $viewData, ChannelPriceHistoryConfigInterface::class); + + /** @var \Traversable $traversableForms */ + $traversableForms = $forms; + $forms = iterator_to_array($traversableForms); + + $channelPriceHistoryConfig->clearTaxonsExcludedFromShowingLowestPrice(); + + /** @var Collection $excludedTaxons */ + $excludedTaxons = $forms['taxonsExcludedFromShowingLowestPrice']->getData(); + + /** @var TaxonInterface $taxon */ + foreach ($excludedTaxons as $taxon) { + $channelPriceHistoryConfig->addTaxonExcludedFromShowingLowestPrice($taxon); + } + + unset($forms['taxonsExcludedFromShowingLowestPrice']); + + $this->propertyPathDataMapper->mapFormsToData(new ArrayCollection($forms), $viewData); + } + + public function getBlockPrefix(): string + { + return 'sylius_channel_price_history_config'; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/Checkout/AddressType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/Checkout/AddressType.php index 579813719f..fc42c2a231 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Type/Checkout/AddressType.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/Checkout/AddressType.php @@ -40,12 +40,11 @@ final class AddressType extends AbstractResourceType parent::__construct($dataClass, $validationGroups); if (null === $addressComparator) { - @trigger_error( - sprintf( - 'Not passing an $addressComparator to "%s" constructor is deprecated since Sylius 1.8 and will be impossible in Sylius 2.0.', - __CLASS__, - ), - \E_USER_DEPRECATED, + trigger_deprecation( + 'sylius/core-bundle', + '1.8', + 'Not passing an $addressComparator to "%s" constructor is deprecated and will be prohibited in Sylius 2.0.', + self::class, ); } diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/ContainsProductConfigurationType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/ContainsProductConfigurationType.php index a1a661ac7f..3fb7c956f0 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/ContainsProductConfigurationType.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/ContainsProductConfigurationType.php @@ -19,8 +19,6 @@ use Sylius\Component\Resource\Repository\RepositoryInterface; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\ReversedTransformer; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class ContainsProductConfigurationType extends AbstractType { @@ -33,10 +31,6 @@ final class ContainsProductConfigurationType extends AbstractType $builder ->add('product_code', ProductAutocompleteChoiceType::class, [ 'label' => 'sylius.form.promotion_action.add_product_configuration.product', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'string', 'groups' => ['sylius']]), - ], ]) ; diff --git a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/NthOrderConfigurationType.php b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/NthOrderConfigurationType.php index 38ab00aac2..a4a9843370 100644 --- a/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/NthOrderConfigurationType.php +++ b/src/Sylius/Bundle/CoreBundle/Form/Type/Promotion/Rule/NthOrderConfigurationType.php @@ -16,8 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Form\Type\Promotion\Rule; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class NthOrderConfigurationType extends AbstractType { @@ -26,10 +24,6 @@ final class NthOrderConfigurationType extends AbstractType $builder ->add('nth', IntegerType::class, [ 'label' => 'sylius.form.promotion_rule.nth_order_configuration.nth', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - ], ]) ; } diff --git a/src/Sylius/Bundle/CoreBundle/Installer/Provider/DatabaseSetupCommandsProvider.php b/src/Sylius/Bundle/CoreBundle/Installer/Provider/DatabaseSetupCommandsProvider.php index 243f7db675..30c1ffe4f3 100644 --- a/src/Sylius/Bundle/CoreBundle/Installer/Provider/DatabaseSetupCommandsProvider.php +++ b/src/Sylius/Bundle/CoreBundle/Installer/Provider/DatabaseSetupCommandsProvider.php @@ -14,6 +14,8 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Installer\Provider; use Doctrine\Bundle\DoctrineBundle\Registry; +use Doctrine\DBAL\Exception; +use Doctrine\DBAL\Platforms\MySQLPlatform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\ORM\EntityManagerInterface; @@ -26,13 +28,30 @@ use Webmozart\Assert\Assert; final class DatabaseSetupCommandsProvider implements DatabaseSetupCommandsProviderInterface { - private bool $isPostgreSQLPlatform; + /** @var AbstractSchemaManager|null */ + private ?AbstractSchemaManager $schemaManager = null; - public function __construct(private Registry $doctrineRegistry) + public function __construct(private EntityManagerInterface|Registry $entityManager) { - $this->isPostgreSQLPlatform = $this->isPostgreSQLPlatform(); + if ($this->entityManager instanceof Registry) { + trigger_deprecation( + 'sylius/sylius', + '1.13', + 'Passing a $registry to the "%s" constructor is deprecated and will be prohibited in Sylius 2.0. Pass an instance of "%s" instead.', + self::class, + EntityManagerInterface::class, + ); + + $objectManager = $this->entityManager->getManager(); + Assert::isInstanceOf($objectManager, EntityManagerInterface::class); + + $this->entityManager = $objectManager; + } } + /** + * @return array + */ public function getCommands(InputInterface $input, OutputInterface $output, QuestionHelper $questionHelper): array { $outputStyle = new SymfonyStyle($input, $output); @@ -43,7 +62,11 @@ final class DatabaseSetupCommandsProvider implements DatabaseSetupCommandsProvid $question = new ConfirmationQuestion('Do you want to drop all of them? (y/N) ', false); if ($questionHelper->ask($input, $output, $question)) { - return $this->dropSchemaAndGetMigrateOrSchemaCreateCommands($outputStyle); + return [ + 'doctrine:schema:drop' => ['--force' => true], + 'doctrine:migrations:version' => ['--delete' => true, '--all' => true, '--no-interaction' => true], + 'doctrine:migrations:migrate' => ['--no-interaction' => true], + ]; } return []; @@ -54,16 +77,19 @@ final class DatabaseSetupCommandsProvider implements DatabaseSetupCommandsProvid sprintf('The database %s already exists and it has no tables.', $this->getDatabaseName()), ); - return $this->getCreateSchemaOrRunMigrationsCommand($outputStyle); + return ['doctrine:migrations:migrate' => ['--no-interaction' => true]]; } - return $this->getCreateDatabaseWithSchemaCommands($outputStyle); + return [ + 'doctrine:database:create' => [], + 'doctrine:migrations:migrate' => ['--no-interaction' => true], + ]; } private function isEmptyDatabasePresent(): bool { try { - return 0 === count($this->getSchemaManager()->listTableNames()); + return 0 === count($this->createSchemaManager()->listTableNames()); } catch (\Exception) { return false; } @@ -72,7 +98,7 @@ final class DatabaseSetupCommandsProvider implements DatabaseSetupCommandsProvid private function isSchemaHasAnyTable(): bool { try { - return 0 !== count($this->getSchemaManager()->listTableNames()); + return 0 !== count($this->createSchemaManager()->listTableNames()); } catch (\Exception) { return false; } @@ -80,91 +106,20 @@ final class DatabaseSetupCommandsProvider implements DatabaseSetupCommandsProvid private function getDatabaseName(): string { - return $this->getEntityManager()->getConnection()->getDatabase(); + return $this->entityManager->getConnection()->getDatabase(); } - private function isPostgreSQLPlatform(): bool + /** + * @return AbstractSchemaManager + * + * @throws Exception + */ + private function createSchemaManager(): AbstractSchemaManager { - return $this->getEntityManager()->getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform; - } - - private function getSchemaManager(): AbstractSchemaManager - { - $connection = $this->getEntityManager()->getConnection(); - - if (method_exists($connection, 'createSchemaManager')) { - return $connection->createSchemaManager(); + if (null === $this->schemaManager) { + $this->schemaManager = $this->entityManager->getConnection()->createSchemaManager(); } - if (method_exists($connection, 'getSchemaManager')) { - return $connection->getSchemaManager(); - } - - throw new \RuntimeException('Unable to get schema manager.'); - } - - private function getEntityManager(): EntityManagerInterface - { - $objectManager = $this->doctrineRegistry->getManager(); - Assert::isInstanceOf($objectManager, EntityManagerInterface::class); - - return $objectManager; - } - - private function getCreateDatabaseWithSchemaCommands(SymfonyStyle $outputStyle): array - { - if ($this->isPostgreSQLPlatform) { - $outputStyle->writeln([ - 'As you\'re using PostgreSQL, we will create a database and schema instead of running migrations.', - 'They will be available starting from Sylius 1.13.', - ]); - - return [ - 'doctrine:database:create', - 'doctrine:schema:create', - ]; - } - - return [ - 'doctrine:database:create', - 'doctrine:migrations:migrate' => ['--no-interaction' => true], - ]; - } - - /** To refactor in Sylius 1.13 */ - private function getCreateSchemaOrRunMigrationsCommand(SymfonyStyle $outputStyle): array - { - if ($this->isPostgreSQLPlatform) { - $outputStyle->writeln([ - 'As you\'re using PostgreSQL, we will create a schema instead of running migrations.', - 'They will be available starting from Sylius 1.13.', - ]); - - return ['doctrine:schema:create']; - } - - return ['doctrine:migrations:migrate' => ['--no-interaction' => true]]; - } - - /** To refactor in Sylius 1.13 */ - private function dropSchemaAndGetMigrateOrSchemaCreateCommands(SymfonyStyle $outputStyle): array - { - if ($this->isPostgreSQLPlatform) { - $outputStyle->writeln([ - 'As you\'re using PostgreSQL, we will drop and create a schema instead of running migrations.', - 'They will be available starting from Sylius 1.13.', - ]); - - return [ - 'doctrine:schema:drop' => ['--force' => true], - 'doctrine:schema:create', - ]; - } - - return [ - 'doctrine:schema:drop' => ['--force' => true], - 'doctrine:migrations:version' => ['--delete' => true, '--all' => true, '--no-interaction' => true], - 'doctrine:migrations:migrate' => ['--no-interaction' => true], - ]; + return $this->schemaManager; } } diff --git a/src/Sylius/Bundle/CoreBundle/Installer/Requirement/FilesystemRequirements.php b/src/Sylius/Bundle/CoreBundle/Installer/Requirement/FilesystemRequirements.php index 06cb4b9269..7d5293a257 100644 --- a/src/Sylius/Bundle/CoreBundle/Installer/Requirement/FilesystemRequirements.php +++ b/src/Sylius/Bundle/CoreBundle/Installer/Requirement/FilesystemRequirements.php @@ -25,11 +25,12 @@ final class FilesystemRequirements extends RequirementCollection parent::__construct($translator->trans('sylius.installer.filesystem.header', [])); if (func_num_args() >= 4) { - @trigger_error(sprintf( - 'Passing root directory to "%s" constructor as the second argument is deprecated since 1.2 ' . - 'and this argument will be removed in 2.0.', + trigger_deprecation( + 'sylius/core-bundle', + '1.2', + 'Passing root directory to "%s" constructor as the second argument is deprecated and this argument will be removed in Sylius 2.0.', self::class, - ), \E_USER_DEPRECATED); + ); [$rootDir, $cacheDir, $logsDir] = [$cacheDir, $logsDir, $rootDir]; } diff --git a/src/Sylius/Bundle/CoreBundle/Installer/Requirement/RequirementCollection.php b/src/Sylius/Bundle/CoreBundle/Installer/Requirement/RequirementCollection.php index e2c6462039..e97daa55c9 100644 --- a/src/Sylius/Bundle/CoreBundle/Installer/Requirement/RequirementCollection.php +++ b/src/Sylius/Bundle/CoreBundle/Installer/Requirement/RequirementCollection.php @@ -13,6 +13,9 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Installer\Requirement; +/** + * @implements \IteratorAggregate + */ abstract class RequirementCollection implements \IteratorAggregate { /** @var array|Requirement[] */ diff --git a/src/Sylius/Bundle/CoreBundle/Installer/Requirement/SyliusRequirements.php b/src/Sylius/Bundle/CoreBundle/Installer/Requirement/SyliusRequirements.php index 340d61a0f2..d3985ab804 100644 --- a/src/Sylius/Bundle/CoreBundle/Installer/Requirement/SyliusRequirements.php +++ b/src/Sylius/Bundle/CoreBundle/Installer/Requirement/SyliusRequirements.php @@ -13,6 +13,9 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Installer\Requirement; +/** + * @implements \IteratorAggregate + */ final class SyliusRequirements implements \IteratorAggregate { /** @var array|RequirementCollection[] */ diff --git a/src/Sylius/Bundle/CoreBundle/Installer/Setup/LocaleSetup.php b/src/Sylius/Bundle/CoreBundle/Installer/Setup/LocaleSetup.php index 5ee4435f07..c01061492e 100644 --- a/src/Sylius/Bundle/CoreBundle/Installer/Setup/LocaleSetup.php +++ b/src/Sylius/Bundle/CoreBundle/Installer/Setup/LocaleSetup.php @@ -20,16 +20,29 @@ use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Intl\Exception\MissingResourceException; use Symfony\Component\Intl\Languages; +use Symfony\Component\Yaml\Yaml; final class LocaleSetup implements LocaleSetupInterface { - private string $locale; - - public function __construct(private RepositoryInterface $localeRepository, private FactoryInterface $localeFactory, string $locale) - { + public function __construct( + private RepositoryInterface $localeRepository, + private FactoryInterface $localeFactory, + private string $locale, + private ?Filesystem $filesystem = null, + private ?string $localeParameterFilePath = 'config/parameters.yaml', + ) { $this->locale = trim($locale); + + if (null === $this->filesystem) { + trigger_deprecation('sylius/sylius', '1.13', 'Not passing %s to %s constructor is deprecated. It will be required in Sylius 2.0.', Filesystem::class, self::class); + } + + if (null === $this->localeParameterFilePath) { + trigger_deprecation('sylius/sylius', '1.13', 'Not passing $localeParameterFilePath to %s constructor is deprecated. It will be required in Sylius 2.0.', self::class); + } } public function setup(InputInterface $input, OutputInterface $output, QuestionHelper $questionHelper): LocaleInterface @@ -38,10 +51,6 @@ final class LocaleSetup implements LocaleSetupInterface $output->writeln(sprintf('Adding %s locale.', $code)); - if ($this->locale !== $code) { - $output->writeln('You may also need to add this locale into config/services.yaml configuration.'); - } - /** @var LocaleInterface|null $existingLocale */ $existingLocale = $this->localeRepository->findOneBy(['code' => $code]); if (null !== $existingLocale) { @@ -71,6 +80,8 @@ final class LocaleSetup implements LocaleSetupInterface $name = $this->getLanguageName($code); } + $this->updateLocaleParameter($code, $output); + $output->writeln(sprintf('Adding %s Language.', $name)); return $code; @@ -89,7 +100,9 @@ final class LocaleSetup implements LocaleSetupInterface $region = null; if (count(explode('_', $code, 2)) === 2) { - [$language, $region] = explode('_', $code, 2); + $codeParts = explode('_', $code, 2); + $language = $codeParts[0]; + $region = $codeParts[1] ?? null; } try { @@ -98,4 +111,22 @@ final class LocaleSetup implements LocaleSetupInterface return null; } } + + private function updateLocaleParameter(string $code, OutputInterface $output): void + { + if ( + $this->localeParameterFilePath === null || + $this->filesystem === null || + !$this->filesystem->exists($this->localeParameterFilePath) + ) { + $output->writeln('You may also need to add this locale into config/parameters.yaml configuration.'); + + return; + } + + $content = Yaml::parseFile($this->localeParameterFilePath); + $content['parameters']['locale'] = $code; + + $this->filesystem->dumpFile($this->localeParameterFilePath, Yaml::dump($content)); + } } diff --git a/src/Sylius/Bundle/CoreBundle/Mailer/AccountRegistrationEmailManager.php b/src/Sylius/Bundle/CoreBundle/Mailer/AccountRegistrationEmailManager.php new file mode 100644 index 0000000000..6affda22e7 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Mailer/AccountRegistrationEmailManager.php @@ -0,0 +1,40 @@ +emailSender->send( + Emails::USER_REGISTRATION, + [$user->getEmail()], + [ + 'user' => $user, + 'localeCode' => $localeCode, + 'channel' => $channel, + ], + [], + [$user->getEmail()], + ); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Mailer/AccountRegistrationEmailManagerInterface.php b/src/Sylius/Bundle/CoreBundle/Mailer/AccountRegistrationEmailManagerInterface.php new file mode 100644 index 0000000000..98b09e4b70 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Mailer/AccountRegistrationEmailManagerInterface.php @@ -0,0 +1,22 @@ +emailSender->send( + code: Emails::ACCOUNT_VERIFICATION, + recipients: [$user->getEmail()], + data: [ + 'user' => $user, + 'localeCode' => $localeCode, + 'channel' => $channel, + ], + ); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Mailer/AccountVerificationEmailManagerInterface.php b/src/Sylius/Bundle/CoreBundle/Mailer/AccountVerificationEmailManagerInterface.php new file mode 100644 index 0000000000..133848aa8b --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Mailer/AccountVerificationEmailManagerInterface.php @@ -0,0 +1,22 @@ +emailSender->send( + Emails::CONTACT_REQUEST, + $recipients, + [ + 'data' => $data, + 'channel' => $channel, + 'localeCode' => $localeCode, + ], + [], + [$data['email']], + ); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Mailer/ContactEmailManagerInterface.php b/src/Sylius/Bundle/CoreBundle/Mailer/ContactEmailManagerInterface.php new file mode 100644 index 0000000000..ed85369b31 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Mailer/ContactEmailManagerInterface.php @@ -0,0 +1,30 @@ + $data + * @param array $recipients + */ + public function sendContactRequest( + array $data, + array $recipients, + ChannelInterface $channel, + string $localeCode, + ): void; +} diff --git a/src/Sylius/Bundle/CoreBundle/Mailer/Emails.php b/src/Sylius/Bundle/CoreBundle/Mailer/Emails.php index c6f5e85f34..87c0b70e02 100644 --- a/src/Sylius/Bundle/CoreBundle/Mailer/Emails.php +++ b/src/Sylius/Bundle/CoreBundle/Mailer/Emails.php @@ -23,11 +23,18 @@ interface Emails public const SHIPMENT_CONFIRMATION = 'shipment_confirmation'; + public const SHIPMENT_CONFIRMATION_RESENT = 'shipment_confirmation_resent'; + public const USER_REGISTRATION = 'user_registration'; public const PASSWORD_RESET = 'password_reset'; public const ADMIN_PASSWORD_RESET = 'admin_password_reset'; + /** + * @deprecated Since Sylius 1.13 and will be removed in Sylius 2.0. Use ACCOUNT_VERIFICATION instead. + */ public const ACCOUNT_VERIFICATION_TOKEN = 'account_verification_token'; + + public const ACCOUNT_VERIFICATION = 'account_verification'; } diff --git a/src/Sylius/Bundle/CoreBundle/Mailer/OrderEmailManager.php b/src/Sylius/Bundle/CoreBundle/Mailer/OrderEmailManager.php index 9b709a7647..65cbf0b103 100644 --- a/src/Sylius/Bundle/CoreBundle/Mailer/OrderEmailManager.php +++ b/src/Sylius/Bundle/CoreBundle/Mailer/OrderEmailManager.php @@ -41,4 +41,20 @@ final class OrderEmailManager implements OrderEmailManagerInterface ], ); } + + public function resendConfirmationEmail(OrderInterface $order): void + { + $email = $order->getCustomer()->getEmail(); + Assert::notNull($email); + + $this->emailSender->send( + Emails::ORDER_CONFIRMATION_RESENT, + [$email], + [ + 'order' => $order, + 'channel' => $order->getChannel(), + 'localeCode' => $order->getLocaleCode(), + ], + ); + } } diff --git a/src/Sylius/Bundle/CoreBundle/Mailer/OrderEmailManagerInterface.php b/src/Sylius/Bundle/CoreBundle/Mailer/OrderEmailManagerInterface.php index 4b998760fe..dce95e885d 100644 --- a/src/Sylius/Bundle/CoreBundle/Mailer/OrderEmailManagerInterface.php +++ b/src/Sylius/Bundle/CoreBundle/Mailer/OrderEmailManagerInterface.php @@ -18,4 +18,6 @@ use Sylius\Component\Core\Model\OrderInterface; interface OrderEmailManagerInterface { public function sendConfirmationEmail(OrderInterface $order): void; + + public function resendConfirmationEmail(OrderInterface $order): void; } diff --git a/src/Sylius/Bundle/CoreBundle/Mailer/ResetPasswordEmailManager.php b/src/Sylius/Bundle/CoreBundle/Mailer/ResetPasswordEmailManager.php new file mode 100644 index 0000000000..be125bc497 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Mailer/ResetPasswordEmailManager.php @@ -0,0 +1,50 @@ +emailSender->send( + code: Emails::ADMIN_PASSWORD_RESET, + recipients: [$user->getEmail()], + data: [ + 'adminUser' => $user, + 'localeCode' => $localCode, + ], + ); + } + + public function sendResetPasswordEmail(UserInterface $user, ChannelInterface $channel, string $localCode): void + { + $this->emailSender->send( + code: Emails::PASSWORD_RESET, + recipients: [$user->getEmail()], + data: [ + 'user' => $user, + 'localeCode' => $localCode, + 'channel' => $channel, + ], + ); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Mailer/ResetPasswordEmailManagerInterface.php b/src/Sylius/Bundle/CoreBundle/Mailer/ResetPasswordEmailManagerInterface.php new file mode 100644 index 0000000000..fc8a25d9a3 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Mailer/ResetPasswordEmailManagerInterface.php @@ -0,0 +1,24 @@ +getOrder(); + $email = $order->getCustomer()->getEmail(); + Assert::notNull($email); + + $this->emailSender->send( + Emails::SHIPMENT_CONFIRMATION, + [$email], + [ + 'shipment' => $shipment, + 'order' => $order, + 'channel' => $order->getChannel(), + 'localeCode' => $order->getLocaleCode(), + ], + ); + } + + public function resendConfirmationEmail(ShipmentInterface $shipment): void + { + /** @var OrderInterface $order */ + $order = $shipment->getOrder(); + Assert::notNull($order); + $email = $order->getCustomer()->getEmail(); + Assert::notNull($email); + + $this->emailSender->send( + Emails::SHIPMENT_CONFIRMATION_RESENT, + [$email], + [ + 'shipment' => $shipment, + 'order' => $order, + 'channel' => $order->getChannel(), + 'localeCode' => $order->getLocaleCode(), + ], + ); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Mailer/ShipmentEmailManagerInterface.php b/src/Sylius/Bundle/CoreBundle/Mailer/ShipmentEmailManagerInterface.php new file mode 100644 index 0000000000..bf0f8e9966 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Mailer/ShipmentEmailManagerInterface.php @@ -0,0 +1,23 @@ +orderTokenValue; + } + + public function setOrderTokenValue(?string $orderTokenValue): void + { + $this->orderTokenValue = $orderTokenValue; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Message/ResendShipmentConfirmationEmail.php b/src/Sylius/Bundle/CoreBundle/Message/ResendShipmentConfirmationEmail.php new file mode 100644 index 0000000000..c27c81b525 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Message/ResendShipmentConfirmationEmail.php @@ -0,0 +1,31 @@ +shipmentId; + } + + public function setShipmentId(?int $shipmentId): void + { + $this->shipmentId = $shipmentId; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/MessageDispatcher/ResendOrderConfirmationEmailDispatcher.php b/src/Sylius/Bundle/CoreBundle/MessageDispatcher/ResendOrderConfirmationEmailDispatcher.php new file mode 100644 index 0000000000..6d1e7f6d63 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/MessageDispatcher/ResendOrderConfirmationEmailDispatcher.php @@ -0,0 +1,30 @@ +messageBus->dispatch(new ResendOrderConfirmationEmail($order->getTokenValue())); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/MessageDispatcher/ResendOrderConfirmationEmailDispatcherInterface.php b/src/Sylius/Bundle/CoreBundle/MessageDispatcher/ResendOrderConfirmationEmailDispatcherInterface.php new file mode 100644 index 0000000000..5ea0dba4f4 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/MessageDispatcher/ResendOrderConfirmationEmailDispatcherInterface.php @@ -0,0 +1,21 @@ +messageBus->dispatch(new ResendShipmentConfirmationEmail($shipment->getId())); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/MessageDispatcher/ResendShipmentConfirmationEmailDispatcherInterface.php b/src/Sylius/Bundle/CoreBundle/MessageDispatcher/ResendShipmentConfirmationEmailDispatcherInterface.php new file mode 100644 index 0000000000..b3ab21e77e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/MessageDispatcher/ResendShipmentConfirmationEmailDispatcherInterface.php @@ -0,0 +1,21 @@ + $userRepository + */ public function __construct( private UserRepositoryInterface $userRepository, - private SenderInterface $sender, + private ResetPasswordEmailManagerInterface $resetPasswordEmailManager, ) { } @@ -33,13 +36,6 @@ final class SendResetPasswordEmailHandler implements MessageHandlerInterface $adminUser = $this->userRepository->findOneByEmail($sendResetPasswordEmail->email); Assert::notNull($adminUser); - $this->sender->send( - Emails::ADMIN_PASSWORD_RESET, - [$sendResetPasswordEmail->email], - [ - 'adminUser' => $adminUser, - 'localeCode' => $sendResetPasswordEmail->localeCode, - ], - ); + $this->resetPasswordEmailManager->sendAdminResetPasswordEmail($adminUser, $sendResetPasswordEmail->localeCode); } } diff --git a/src/Sylius/Bundle/CoreBundle/MessageHandler/ResendOrderConfirmationEmailHandler.php b/src/Sylius/Bundle/CoreBundle/MessageHandler/ResendOrderConfirmationEmailHandler.php new file mode 100644 index 0000000000..8bfe130df0 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/MessageHandler/ResendOrderConfirmationEmailHandler.php @@ -0,0 +1,44 @@ + $orderRepository + */ + public function __construct( + private OrderEmailManagerInterface $orderEmailManager, + private RepositoryInterface $orderRepository, + ) { + } + + public function __invoke(ResendOrderConfirmationEmail $resendOrderConfirmation): void + { + /** @var OrderInterface|null $order */ + $order = $this->orderRepository->findOneBy(['tokenValue' => $resendOrderConfirmation->getOrderTokenValue()]); + if ($order === null) { + throw new NotFoundHttpException(sprintf('The order with tokenValue %s has not been found', $resendOrderConfirmation->getOrderTokenValue())); + } + + $this->orderEmailManager->resendConfirmationEmail($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/MessageHandler/ResendShipmentConfirmationEmailHandler.php b/src/Sylius/Bundle/CoreBundle/MessageHandler/ResendShipmentConfirmationEmailHandler.php new file mode 100644 index 0000000000..f9b38d5e7e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/MessageHandler/ResendShipmentConfirmationEmailHandler.php @@ -0,0 +1,44 @@ + $shipmentRepository + */ + public function __construct( + private RepositoryInterface $shipmentRepository, + private ShipmentEmailManagerInterface $shipmentEmailManager, + ) { + } + + public function __invoke(ResendShipmentConfirmationEmail $resendShipmentConfirmationEmail): void + { + /** @var ShipmentInterface|null $shipment */ + $shipment = $this->shipmentRepository->find($resendShipmentConfirmationEmail->getShipmentId()); + if (null === $shipment) { + throw new NotFoundHttpException(sprintf('Shipment with id "%s" does not exist.', $resendShipmentConfirmationEmail->getShipmentId())); + } + + $this->shipmentEmailManager->resendConfirmationEmail($shipment); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20190109095211.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20190109095211.php index 569c9126ae..57f2187da8 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20190109095211.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20190109095211.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20190109095211 extends AbstractMigration { public function up(Schema $schema): void diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20190109160409.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20190109160409.php index d1e92745f5..8211141445 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20190109160409.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20190109160409.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20190109160409 extends AbstractMigration { public function up(Schema $schema): void diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20190204092544.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20190204092544.php index 0853ff9ede..a3fbbe3810 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20190204092544.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20190204092544.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20190204092544 extends AbstractMigration { public function up(Schema $schema): void diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20190416073011.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20190416073011.php index eaad8a4c71..506e05a799 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20190416073011.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20190416073011.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20190416073011 extends AbstractMigration { public function up(Schema $schema): void diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20200122082429.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20200122082429.php index 823e348087..16b0c08222 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20200122082429.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20200122082429.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20200122082429 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210311142134.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210311142134.php index 99773dcc98..ba6ebb4572 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210311142134.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210311142134.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20210311142134 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210819203611.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210819203611.php index 9fbb0c98bc..804fb2ccbe 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210819203611.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210819203611.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20210819203611 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210824132538.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210824132538.php index 22ddaf225a..069cb0a8ab 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210824132538.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210824132538.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20210824132538 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210826063828.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210826063828.php index cb9bc2be38..df91b2f6b7 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210826063828.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210826063828.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20210826063828 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210830193340.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210830193340.php index 817d0dfcd5..795f51aa23 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210830193340.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210830193340.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20210830193340 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210921093619.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210921093619.php index dccfbca79e..0e9f85d3bf 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20210921093619.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20210921093619.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20210921093619 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211001073918.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211001073918.php index a13d49a5df..b303470b9a 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211001073918.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211001073918.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20211001073918 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211006182150.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211006182150.php index f0898e62e4..de1a2ec0fe 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211006182150.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211006182150.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20211006182150 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211025082311.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211025082311.php index 41a0656a59..49df89dcc0 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211025082311.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211025082311.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20211025082311 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211028150911.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211028150911.php index 6233bf7bd7..f2046e32c8 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211028150911.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211028150911.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20211028150911 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211122104644.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211122104644.php index 55f380077e..82e825e16e 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211122104644.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211122104644.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20211122104644 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211125085254.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211125085254.php index 2bff96b7d1..85283329ba 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211125085254.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211125085254.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20211125085254 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211125122631.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211125122631.php index aed1ad0350..b5d866fe43 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211125122631.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211125122631.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20211125122631 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211129213836.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211129213836.php index bcca69d921..212d53b969 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20211129213836.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20211129213836.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20211129213836 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220127150747.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220127150747.php index 37acff6906..cdee1636e6 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220127150747.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220127150747.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20220127150747 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220203115813.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220203115813.php index 9c3afb74a6..ff3df3f705 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220203115813.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220203115813.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20220203115813 extends AbstractMigration { private const OLD_TRANSPORT_DSN = 'MESSENGER_TRANSPORT_DSN'; diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220210135918.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220210135918.php index ac558d7b3c..64c087c673 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220210135918.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220210135918.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20220210135918 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220407131547.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220407131547.php index 1287a1b8aa..fb672d8495 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220407131547.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220407131547.php @@ -51,11 +51,8 @@ final class Version20220407131547 extends AbstractMigration private function getExistingIndexesNames(string $tableName): array { - if (method_exists($this->connection, 'createSchemaManager')) { - $indexes = $this->connection->createSchemaManager()->listTableIndexes($tableName); - } else { - $indexes = $this->connection->getSchemaManager()->listTableIndexes($tableName); - } + $indexes = $this->connection->createSchemaManager()->listTableIndexes($tableName); + $indexesNames = []; foreach ($indexes as $index) { diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220614124639.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220614124639.php index f6ba378184..137c3e77f2 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220614124639.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220614124639.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20220614124639 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220803125615.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220803125615.php index 254885f11d..af0e9b0f82 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220803125615.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220803125615.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20220803125615 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220912091947.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220912091947.php index 91bf177884..fe0187e26d 100644 --- a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220912091947.php +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220912091947.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\CoreBundle\Migrations; use Doctrine\DBAL\Schema\Schema; use Sylius\Bundle\CoreBundle\Doctrine\Migrations\AbstractMigration; -/** - * Auto-generated Migration: Please modify to your needs! - */ final class Version20220912091947 extends AbstractMigration { public function getDescription(): string diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20220926113252.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220926113252.php new file mode 100644 index 0000000000..83e4cae2d9 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20220926113252.php @@ -0,0 +1,37 @@ +addSql('CREATE TABLE sylius_promotion_translation (id INT AUTO_INCREMENT NOT NULL, translatable_id INT NOT NULL, label VARCHAR(255) DEFAULT NULL, locale VARCHAR(255) NOT NULL, INDEX IDX_3C7A76182C2AC5D3 (translatable_id), UNIQUE INDEX sylius_promotion_translation_uniq_trans (translatable_id, locale), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE sylius_promotion_translation ADD CONSTRAINT FK_3C7A76182C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_promotion (id) ON DELETE CASCADE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_promotion_translation DROP FOREIGN KEY FK_3C7A76182C2AC5D3'); + $this->addSql('DROP TABLE sylius_promotion_translation'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20230327121633.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230327121633.php new file mode 100644 index 0000000000..512f1fda40 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230327121633.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE sylius_user_oauth CHANGE access_token access_token TEXT DEFAULT NULL, CHANGE refresh_token refresh_token TEXT DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_user_oauth CHANGE access_token access_token VARCHAR(255) DEFAULT NULL, CHANGE refresh_token refresh_token VARCHAR(255) DEFAULT NULL'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20230331091850.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230331091850.php new file mode 100644 index 0000000000..36b07f7aec --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230331091850.php @@ -0,0 +1,73 @@ +skipIf( + $schema->hasTable('sylius_channel_pricing_log_entry'), + 'Skipping migration: Sylius price history migrations were previously executed.', + ); + + $this->addSql('CREATE TABLE sylius_channel_price_history_config (id INT AUTO_INCREMENT NOT NULL, lowest_price_for_discounted_products_checking_period INT DEFAULT 30 NOT NULL, lowest_price_for_discounted_products_visible TINYINT(1) DEFAULT 1 NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE sylius_channel_price_history_config_excluded_taxons (channel_id INT NOT NULL, taxon_id INT NOT NULL, INDEX IDX_77FD02A72F5A1AA (channel_id), INDEX IDX_77FD02ADE13F470 (taxon_id), PRIMARY KEY(channel_id, taxon_id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE sylius_channel_pricing_log_entry (id INT AUTO_INCREMENT NOT NULL, channel_pricing_id INT NOT NULL, price INT NOT NULL, original_price INT DEFAULT NULL, logged_at DATETIME NOT NULL, INDEX IDX_77181A53EADFFE5 (channel_pricing_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE sylius_channel_price_history_config_excluded_taxons ADD CONSTRAINT FK_77FD02A72F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel_price_history_config (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE sylius_channel_price_history_config_excluded_taxons ADD CONSTRAINT FK_77FD02ADE13F470 FOREIGN KEY (taxon_id) REFERENCES sylius_taxon (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE sylius_channel_pricing_log_entry ADD CONSTRAINT FK_77181A53EADFFE5 FOREIGN KEY (channel_pricing_id) REFERENCES sylius_channel_pricing (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE sylius_channel ADD channel_price_history_config_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE sylius_channel ADD CONSTRAINT FK_16C8119E75F20EAE FOREIGN KEY (channel_price_history_config_id) REFERENCES sylius_channel_price_history_config (id) ON DELETE CASCADE'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_16C8119E75F20EAE ON sylius_channel (channel_price_history_config_id)'); + $this->addSql('ALTER TABLE sylius_channel_pricing ADD lowest_price_before_discount INT DEFAULT NULL'); + + /** Create an initial log state based on the price of products at the time of migration processing */ + $this->addSql('INSERT INTO `sylius_channel_pricing_log_entry` (`channel_pricing_id`, `price`, `original_price`, `logged_at`) SELECT `id`, `price`, `original_price`, NOW() FROM `sylius_channel_pricing`'); + } + + public function postUp(Schema $schema): void + { + $channelsIds = $this->connection->executeQuery('SELECT id from sylius_channel WHERE channel_price_history_config_id IS NULL')->fetchAllAssociative(); + foreach ($channelsIds as $channelId) { + $this->connection->executeQuery('INSERT INTO sylius_channel_price_history_config (lowest_price_for_discounted_products_checking_period, lowest_price_for_discounted_products_visible) VALUES (30, true)'); + $this->connection->executeQuery('UPDATE sylius_channel SET channel_price_history_config_id = :priceHistoryConfig WHERE id = :channel', [ + 'channel' => $channelId['id'], + 'priceHistoryConfig' => $this->connection->lastInsertId(), + ]); + } + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_channel DROP FOREIGN KEY FK_16C8119E75F20EAE'); + $this->addSql('ALTER TABLE sylius_channel_price_history_config_excluded_taxons DROP FOREIGN KEY FK_77FD02A72F5A1AA'); + $this->addSql('ALTER TABLE sylius_channel_price_history_config_excluded_taxons DROP FOREIGN KEY FK_77FD02ADE13F470'); + $this->addSql('ALTER TABLE sylius_channel_pricing_log_entry DROP FOREIGN KEY FK_77181A53EADFFE5'); + $this->addSql('DROP TABLE sylius_channel_price_history_config'); + $this->addSql('DROP TABLE sylius_channel_price_history_config_excluded_taxons'); + $this->addSql('DROP TABLE sylius_channel_pricing_log_entry'); + $this->addSql('DROP INDEX UNIQ_16C8119E75F20EAE ON sylius_channel'); + $this->addSql('ALTER TABLE sylius_channel DROP channel_price_history_config_id'); + $this->addSql('ALTER TABLE sylius_channel_pricing DROP lowest_price_before_discount'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20230419092354.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230419092354.php new file mode 100644 index 0000000000..5451f774d0 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230419092354.php @@ -0,0 +1,689 @@ +hasTable('sylius_address')) { + $this->markAsExecuted($this->getVersion()); + $this->skipIf(true, 'This migration is marked as completed.'); + } + + $this->addSql('CREATE SEQUENCE sylius_address_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_address_log_entries_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_adjustment_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_admin_user_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_avatar_image_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_catalog_promotion_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_catalog_promotion_action_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_catalog_promotion_scope_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_catalog_promotion_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_channel_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_channel_pricing_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_country_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_currency_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_customer_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_customer_group_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_exchange_rate_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_gateway_config_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_locale_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_order_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_order_item_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_order_item_unit_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_order_sequence_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_payment_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_payment_method_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_payment_method_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_association_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_association_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_association_type_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_attribute_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_attribute_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_attribute_value_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_image_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_option_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_option_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_option_value_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_option_value_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_review_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_taxon_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_variant_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_product_variant_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_promotion_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_promotion_action_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_promotion_coupon_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_promotion_rule_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_province_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_shipment_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_shipping_category_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_shipping_method_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_shipping_method_rule_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_shipping_method_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_shop_billing_data_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_shop_user_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_tax_category_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_tax_rate_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_taxon_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_taxon_image_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_taxon_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_user_oauth_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_zone_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_zone_member_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE sylius_address (id INT NOT NULL, customer_id INT DEFAULT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, phone_number VARCHAR(255) DEFAULT NULL, street VARCHAR(255) NOT NULL, company VARCHAR(255) DEFAULT NULL, city VARCHAR(255) NOT NULL, postcode VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, country_code VARCHAR(255) NOT NULL, province_code VARCHAR(255) DEFAULT NULL, province_name VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_B97FF0589395C3F3 ON sylius_address (customer_id)'); + $this->addSql('CREATE TABLE sylius_address_log_entries (id INT NOT NULL, action VARCHAR(255) NOT NULL, logged_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, object_id VARCHAR(64) DEFAULT NULL, object_class VARCHAR(255) NOT NULL, version INT NOT NULL, data TEXT NOT NULL, username VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('COMMENT ON COLUMN sylius_address_log_entries.data IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE sylius_adjustment (id INT NOT NULL, order_id INT DEFAULT NULL, order_item_id INT DEFAULT NULL, order_item_unit_id INT DEFAULT NULL, shipment_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, label VARCHAR(255) DEFAULT NULL, amount INT NOT NULL, is_neutral BOOLEAN NOT NULL, is_locked BOOLEAN NOT NULL, origin_code VARCHAR(255) DEFAULT NULL, details JSON NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_ACA6E0F28D9F6D38 ON sylius_adjustment (order_id)'); + $this->addSql('CREATE INDEX IDX_ACA6E0F2E415FB15 ON sylius_adjustment (order_item_id)'); + $this->addSql('CREATE INDEX IDX_ACA6E0F2F720C233 ON sylius_adjustment (order_item_unit_id)'); + $this->addSql('CREATE INDEX IDX_ACA6E0F27BE036FC ON sylius_adjustment (shipment_id)'); + $this->addSql('CREATE TABLE sylius_admin_user (id INT NOT NULL, username VARCHAR(255) DEFAULT NULL, username_canonical VARCHAR(255) DEFAULT NULL, enabled BOOLEAN NOT NULL, salt VARCHAR(255) NOT NULL, password VARCHAR(255) DEFAULT NULL, encoder_name VARCHAR(255) DEFAULT NULL, last_login TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, password_reset_token VARCHAR(255) DEFAULT NULL, password_requested_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, email_verification_token VARCHAR(255) DEFAULT NULL, verified_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, locked BOOLEAN NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, credentials_expire_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, roles TEXT NOT NULL, email VARCHAR(255) DEFAULT NULL, email_canonical VARCHAR(255) DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, first_name VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, locale_code VARCHAR(12) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('COMMENT ON COLUMN sylius_admin_user.roles IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE sylius_avatar_image (id INT NOT NULL, owner_id INT NOT NULL, type VARCHAR(255) DEFAULT NULL, path VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1068A3A97E3C61F9 ON sylius_avatar_image (owner_id)'); + $this->addSql('CREATE TABLE sylius_catalog_promotion (id INT NOT NULL, code VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, start_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, end_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, enabled BOOLEAN NOT NULL, priority INT DEFAULT 0 NOT NULL, exclusive BOOLEAN DEFAULT false NOT NULL, state VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1055865077153098 ON sylius_catalog_promotion (code)'); + $this->addSql('CREATE TABLE sylius_catalog_promotion_channels (catalog_promotion_id INT NOT NULL, channel_id INT NOT NULL, PRIMARY KEY(catalog_promotion_id, channel_id))'); + $this->addSql('CREATE INDEX IDX_48E9AE7622E2CB5A ON sylius_catalog_promotion_channels (catalog_promotion_id)'); + $this->addSql('CREATE INDEX IDX_48E9AE7672F5A1AA ON sylius_catalog_promotion_channels (channel_id)'); + $this->addSql('CREATE TABLE sylius_catalog_promotion_action (id INT NOT NULL, catalog_promotion_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, configuration TEXT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_F529624722E2CB5A ON sylius_catalog_promotion_action (catalog_promotion_id)'); + $this->addSql('COMMENT ON COLUMN sylius_catalog_promotion_action.configuration IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE sylius_catalog_promotion_scope (id INT NOT NULL, promotion_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, configuration TEXT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_584AA86A139DF194 ON sylius_catalog_promotion_scope (promotion_id)'); + $this->addSql('COMMENT ON COLUMN sylius_catalog_promotion_scope.configuration IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE sylius_catalog_promotion_translation (id INT NOT NULL, translatable_id INT NOT NULL, label VARCHAR(255) DEFAULT NULL, description VARCHAR(255) DEFAULT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_BA065D3C2C2AC5D3 ON sylius_catalog_promotion_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX sylius_catalog_promotion_translation_uniq_trans ON sylius_catalog_promotion_translation (translatable_id, locale)'); + $this->addSql('CREATE TABLE sylius_channel (id INT NOT NULL, shop_billing_data_id INT DEFAULT NULL, default_locale_id INT NOT NULL, base_currency_id INT NOT NULL, default_tax_zone_id INT DEFAULT NULL, menu_taxon_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, color VARCHAR(255) DEFAULT NULL, description TEXT DEFAULT NULL, enabled BOOLEAN NOT NULL, hostname VARCHAR(255) DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, theme_name VARCHAR(255) DEFAULT NULL, tax_calculation_strategy VARCHAR(255) NOT NULL, contact_email VARCHAR(255) DEFAULT NULL, contact_phone_number VARCHAR(255) DEFAULT NULL, skipping_shipping_step_allowed BOOLEAN NOT NULL, skipping_payment_step_allowed BOOLEAN NOT NULL, account_verification_required BOOLEAN NOT NULL, shipping_address_in_checkout_required BOOLEAN DEFAULT false NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_16C8119E77153098 ON sylius_channel (code)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_16C8119EB5282EDF ON sylius_channel (shop_billing_data_id)'); + $this->addSql('CREATE INDEX IDX_16C8119E743BF776 ON sylius_channel (default_locale_id)'); + $this->addSql('CREATE INDEX IDX_16C8119E3101778E ON sylius_channel (base_currency_id)'); + $this->addSql('CREATE INDEX IDX_16C8119EA978C17 ON sylius_channel (default_tax_zone_id)'); + $this->addSql('CREATE INDEX IDX_16C8119EF242B1E6 ON sylius_channel (menu_taxon_id)'); + $this->addSql('CREATE INDEX IDX_16C8119EE551C011 ON sylius_channel (hostname)'); + $this->addSql('CREATE TABLE sylius_channel_currencies (channel_id INT NOT NULL, currency_id INT NOT NULL, PRIMARY KEY(channel_id, currency_id))'); + $this->addSql('CREATE INDEX IDX_AE491F9372F5A1AA ON sylius_channel_currencies (channel_id)'); + $this->addSql('CREATE INDEX IDX_AE491F9338248176 ON sylius_channel_currencies (currency_id)'); + $this->addSql('CREATE TABLE sylius_channel_locales (channel_id INT NOT NULL, locale_id INT NOT NULL, PRIMARY KEY(channel_id, locale_id))'); + $this->addSql('CREATE INDEX IDX_786B7A8472F5A1AA ON sylius_channel_locales (channel_id)'); + $this->addSql('CREATE INDEX IDX_786B7A84E559DFD1 ON sylius_channel_locales (locale_id)'); + $this->addSql('CREATE TABLE sylius_channel_countries (channel_id INT NOT NULL, country_id INT NOT NULL, PRIMARY KEY(channel_id, country_id))'); + $this->addSql('CREATE INDEX IDX_D96E51AE72F5A1AA ON sylius_channel_countries (channel_id)'); + $this->addSql('CREATE INDEX IDX_D96E51AEF92F3E70 ON sylius_channel_countries (country_id)'); + $this->addSql('CREATE TABLE sylius_channel_pricing (id INT NOT NULL, product_variant_id INT NOT NULL, price INT DEFAULT NULL, original_price INT DEFAULT NULL, minimum_price INT DEFAULT 0, channel_code VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_7801820CA80EF684 ON sylius_channel_pricing (product_variant_id)'); + $this->addSql('CREATE UNIQUE INDEX product_variant_channel_idx ON sylius_channel_pricing (product_variant_id, channel_code)'); + $this->addSql('CREATE TABLE sylius_channel_pricing_catalog_promotions (channel_pricing_id INT NOT NULL, catalog_promotion_id INT NOT NULL, PRIMARY KEY(channel_pricing_id, catalog_promotion_id))'); + $this->addSql('CREATE INDEX IDX_9F52FF513EADFFE5 ON sylius_channel_pricing_catalog_promotions (channel_pricing_id)'); + $this->addSql('CREATE INDEX IDX_9F52FF5122E2CB5A ON sylius_channel_pricing_catalog_promotions (catalog_promotion_id)'); + $this->addSql('CREATE TABLE sylius_country (id INT NOT NULL, code VARCHAR(2) NOT NULL, enabled BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_E74256BF77153098 ON sylius_country (code)'); + $this->addSql('CREATE INDEX IDX_E74256BF77153098 ON sylius_country (code)'); + $this->addSql('CREATE TABLE sylius_currency (id INT NOT NULL, code VARCHAR(3) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_96EDD3D077153098 ON sylius_currency (code)'); + $this->addSql('CREATE TABLE sylius_customer (id INT NOT NULL, customer_group_id INT DEFAULT NULL, default_address_id INT DEFAULT NULL, email VARCHAR(255) NOT NULL, email_canonical VARCHAR(255) NOT NULL, first_name VARCHAR(255) DEFAULT NULL, last_name VARCHAR(255) DEFAULT NULL, birthday TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, gender VARCHAR(1) DEFAULT \'u\' NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, phone_number VARCHAR(255) DEFAULT NULL, subscribed_to_newsletter BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7E82D5E6E7927C74 ON sylius_customer (email)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7E82D5E6A0D96FBF ON sylius_customer (email_canonical)'); + $this->addSql('CREATE INDEX IDX_7E82D5E6D2919A68 ON sylius_customer (customer_group_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7E82D5E6BD94FB16 ON sylius_customer (default_address_id)'); + $this->addSql('CREATE INDEX created_at_index ON sylius_customer (created_at)'); + $this->addSql('CREATE TABLE sylius_customer_group (id INT NOT NULL, code VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7FCF9B0577153098 ON sylius_customer_group (code)'); + $this->addSql('CREATE TABLE sylius_exchange_rate (id INT NOT NULL, source_currency INT NOT NULL, target_currency INT NOT NULL, ratio NUMERIC(10, 5) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5F52B852A76BEED ON sylius_exchange_rate (source_currency)'); + $this->addSql('CREATE INDEX IDX_5F52B85B3FD5856 ON sylius_exchange_rate (target_currency)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_5F52B852A76BEEDB3FD5856 ON sylius_exchange_rate (source_currency, target_currency)'); + $this->addSql('CREATE TABLE sylius_gateway_config (id INT NOT NULL, gateway_name VARCHAR(255) NOT NULL, factory_name VARCHAR(255) NOT NULL, config JSON NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE sylius_locale (id INT NOT NULL, code VARCHAR(12) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7BA1286477153098 ON sylius_locale (code)'); + $this->addSql('CREATE TABLE sylius_order (id INT NOT NULL, shipping_address_id INT DEFAULT NULL, billing_address_id INT DEFAULT NULL, channel_id INT DEFAULT NULL, promotion_coupon_id INT DEFAULT NULL, customer_id INT DEFAULT NULL, number VARCHAR(255) DEFAULT NULL, notes TEXT DEFAULT NULL, state VARCHAR(255) NOT NULL, checkout_completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, items_total INT NOT NULL, adjustments_total INT NOT NULL, total INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, currency_code VARCHAR(3) NOT NULL, locale_code VARCHAR(255) NOT NULL, checkout_state VARCHAR(255) NOT NULL, payment_state VARCHAR(255) NOT NULL, shipping_state VARCHAR(255) NOT NULL, created_by_guest BOOLEAN DEFAULT true NOT NULL, token_value VARCHAR(255) DEFAULT NULL, customer_ip VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6196A1F996901F54 ON sylius_order (number)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6196A1F9BEA95C75 ON sylius_order (token_value)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6196A1F94D4CFF2B ON sylius_order (shipping_address_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6196A1F979D0C0E4 ON sylius_order (billing_address_id)'); + $this->addSql('CREATE INDEX IDX_6196A1F972F5A1AA ON sylius_order (channel_id)'); + $this->addSql('CREATE INDEX IDX_6196A1F917B24436 ON sylius_order (promotion_coupon_id)'); + $this->addSql('CREATE INDEX IDX_6196A1F99395C3F3 ON sylius_order (customer_id)'); + $this->addSql('CREATE INDEX IDX_6196A1F9A393D2FB43625D9F ON sylius_order (state, updated_at)'); + $this->addSql('CREATE TABLE sylius_promotion_order (order_id INT NOT NULL, promotion_id INT NOT NULL, PRIMARY KEY(order_id, promotion_id))'); + $this->addSql('CREATE INDEX IDX_BF9CF6FB8D9F6D38 ON sylius_promotion_order (order_id)'); + $this->addSql('CREATE INDEX IDX_BF9CF6FB139DF194 ON sylius_promotion_order (promotion_id)'); + $this->addSql('CREATE TABLE sylius_order_item (id INT NOT NULL, order_id INT NOT NULL, variant_id INT NOT NULL, quantity INT NOT NULL, unit_price INT NOT NULL, original_unit_price INT DEFAULT NULL, units_total INT NOT NULL, adjustments_total INT NOT NULL, total INT NOT NULL, is_immutable BOOLEAN NOT NULL, product_name VARCHAR(255) DEFAULT NULL, variant_name VARCHAR(255) DEFAULT NULL, version INT DEFAULT 1 NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_77B587ED8D9F6D38 ON sylius_order_item (order_id)'); + $this->addSql('CREATE INDEX IDX_77B587ED3B69A9AF ON sylius_order_item (variant_id)'); + $this->addSql('CREATE TABLE sylius_order_item_unit (id INT NOT NULL, order_item_id INT NOT NULL, shipment_id INT DEFAULT NULL, adjustments_total INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_82BF226EE415FB15 ON sylius_order_item_unit (order_item_id)'); + $this->addSql('CREATE INDEX IDX_82BF226E7BE036FC ON sylius_order_item_unit (shipment_id)'); + $this->addSql('CREATE TABLE sylius_order_sequence (id INT NOT NULL, idx INT NOT NULL, version INT DEFAULT 1 NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE sylius_payment (id INT NOT NULL, method_id INT DEFAULT NULL, order_id INT NOT NULL, currency_code VARCHAR(3) NOT NULL, amount INT NOT NULL, state VARCHAR(255) NOT NULL, details JSON NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_D9191BD419883967 ON sylius_payment (method_id)'); + $this->addSql('CREATE INDEX IDX_D9191BD48D9F6D38 ON sylius_payment (order_id)'); + $this->addSql('CREATE TABLE sylius_payment_method (id INT NOT NULL, gateway_config_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, environment VARCHAR(255) DEFAULT NULL, is_enabled BOOLEAN NOT NULL, position INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_A75B0B0D77153098 ON sylius_payment_method (code)'); + $this->addSql('CREATE INDEX IDX_A75B0B0DF23D6140 ON sylius_payment_method (gateway_config_id)'); + $this->addSql('CREATE TABLE sylius_payment_method_channels (payment_method_id INT NOT NULL, channel_id INT NOT NULL, PRIMARY KEY(payment_method_id, channel_id))'); + $this->addSql('CREATE INDEX IDX_543AC0CC5AA1164F ON sylius_payment_method_channels (payment_method_id)'); + $this->addSql('CREATE INDEX IDX_543AC0CC72F5A1AA ON sylius_payment_method_channels (channel_id)'); + $this->addSql('CREATE TABLE sylius_payment_method_translation (id INT NOT NULL, translatable_id INT NOT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, instructions TEXT DEFAULT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_966BE3A12C2AC5D3 ON sylius_payment_method_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX sylius_payment_method_translation_uniq_trans ON sylius_payment_method_translation (translatable_id, locale)'); + $this->addSql('CREATE TABLE sylius_payment_security_token (hash VARCHAR(255) NOT NULL, details TEXT DEFAULT NULL, after_url TEXT DEFAULT NULL, target_url TEXT NOT NULL, gateway_name VARCHAR(255) NOT NULL, PRIMARY KEY(hash))'); + $this->addSql('COMMENT ON COLUMN sylius_payment_security_token.details IS \'(DC2Type:object)\''); + $this->addSql('CREATE TABLE sylius_product (id INT NOT NULL, main_taxon_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, enabled BOOLEAN NOT NULL, variant_selection_method VARCHAR(255) NOT NULL, average_rating DOUBLE PRECISION DEFAULT \'0\' NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_677B9B7477153098 ON sylius_product (code)'); + $this->addSql('CREATE INDEX IDX_677B9B74731E505 ON sylius_product (main_taxon_id)'); + $this->addSql('CREATE TABLE sylius_product_channels (product_id INT NOT NULL, channel_id INT NOT NULL, PRIMARY KEY(product_id, channel_id))'); + $this->addSql('CREATE INDEX IDX_F9EF269B4584665A ON sylius_product_channels (product_id)'); + $this->addSql('CREATE INDEX IDX_F9EF269B72F5A1AA ON sylius_product_channels (channel_id)'); + $this->addSql('CREATE TABLE sylius_product_options (product_id INT NOT NULL, option_id INT NOT NULL, PRIMARY KEY(product_id, option_id))'); + $this->addSql('CREATE INDEX IDX_2B5FF0094584665A ON sylius_product_options (product_id)'); + $this->addSql('CREATE INDEX IDX_2B5FF009A7C41D6F ON sylius_product_options (option_id)'); + $this->addSql('CREATE TABLE sylius_product_association (id INT NOT NULL, association_type_id INT NOT NULL, product_id INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_48E9CDABB1E1C39 ON sylius_product_association (association_type_id)'); + $this->addSql('CREATE INDEX IDX_48E9CDAB4584665A ON sylius_product_association (product_id)'); + $this->addSql('CREATE UNIQUE INDEX product_association_idx ON sylius_product_association (product_id, association_type_id)'); + $this->addSql('CREATE TABLE sylius_product_association_product (association_id INT NOT NULL, product_id INT NOT NULL, PRIMARY KEY(association_id, product_id))'); + $this->addSql('CREATE INDEX IDX_A427B983EFB9C8A5 ON sylius_product_association_product (association_id)'); + $this->addSql('CREATE INDEX IDX_A427B9834584665A ON sylius_product_association_product (product_id)'); + $this->addSql('CREATE TABLE sylius_product_association_type (id INT NOT NULL, code VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_CCB8914C77153098 ON sylius_product_association_type (code)'); + $this->addSql('CREATE TABLE sylius_product_association_type_translation (id INT NOT NULL, translatable_id INT NOT NULL, name VARCHAR(255) DEFAULT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_4F618E52C2AC5D3 ON sylius_product_association_type_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX sylius_product_association_type_translation_uniq_trans ON sylius_product_association_type_translation (translatable_id, locale)'); + $this->addSql('CREATE TABLE sylius_product_attribute (id INT NOT NULL, code VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, storage_type VARCHAR(255) NOT NULL, configuration TEXT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, position INT NOT NULL, translatable BOOLEAN DEFAULT true NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_BFAF484A77153098 ON sylius_product_attribute (code)'); + $this->addSql('COMMENT ON COLUMN sylius_product_attribute.configuration IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE sylius_product_attribute_translation (id INT NOT NULL, translatable_id INT NOT NULL, name VARCHAR(255) NOT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_93850EBA2C2AC5D3 ON sylius_product_attribute_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX sylius_product_attribute_translation_uniq_trans ON sylius_product_attribute_translation (translatable_id, locale)'); + $this->addSql('CREATE TABLE sylius_product_attribute_value (id INT NOT NULL, product_id INT NOT NULL, attribute_id INT NOT NULL, locale_code VARCHAR(255) DEFAULT NULL, text_value TEXT DEFAULT NULL, boolean_value BOOLEAN DEFAULT NULL, integer_value INT DEFAULT NULL, float_value DOUBLE PRECISION DEFAULT NULL, datetime_value TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, date_value DATE DEFAULT NULL, json_value JSON DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_8A053E544584665A ON sylius_product_attribute_value (product_id)'); + $this->addSql('CREATE INDEX IDX_8A053E54B6E62EFA ON sylius_product_attribute_value (attribute_id)'); + $this->addSql('CREATE TABLE sylius_product_image (id INT NOT NULL, owner_id INT NOT NULL, type VARCHAR(255) DEFAULT NULL, path VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_88C64B2D7E3C61F9 ON sylius_product_image (owner_id)'); + $this->addSql('CREATE TABLE sylius_product_image_product_variants (image_id INT NOT NULL, variant_id INT NOT NULL, PRIMARY KEY(image_id, variant_id))'); + $this->addSql('CREATE INDEX IDX_8FFDAE8D3DA5256D ON sylius_product_image_product_variants (image_id)'); + $this->addSql('CREATE INDEX IDX_8FFDAE8D3B69A9AF ON sylius_product_image_product_variants (variant_id)'); + $this->addSql('CREATE TABLE sylius_product_option (id INT NOT NULL, code VARCHAR(255) NOT NULL, position INT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_E4C0EBEF77153098 ON sylius_product_option (code)'); + $this->addSql('CREATE TABLE sylius_product_option_translation (id INT NOT NULL, translatable_id INT NOT NULL, name VARCHAR(255) NOT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_CBA491AD2C2AC5D3 ON sylius_product_option_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX sylius_product_option_translation_uniq_trans ON sylius_product_option_translation (translatable_id, locale)'); + $this->addSql('CREATE TABLE sylius_product_option_value (id INT NOT NULL, option_id INT NOT NULL, code VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_F7FF7D4B77153098 ON sylius_product_option_value (code)'); + $this->addSql('CREATE INDEX IDX_F7FF7D4BA7C41D6F ON sylius_product_option_value (option_id)'); + $this->addSql('CREATE TABLE sylius_product_option_value_translation (id INT NOT NULL, translatable_id INT NOT NULL, value VARCHAR(255) NOT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_8D4382DC2C2AC5D3 ON sylius_product_option_value_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX sylius_product_option_value_translation_uniq_trans ON sylius_product_option_value_translation (translatable_id, locale)'); + $this->addSql('CREATE TABLE sylius_product_review (id INT NOT NULL, product_id INT NOT NULL, author_id INT NOT NULL, title VARCHAR(255) DEFAULT NULL, rating INT NOT NULL, comment TEXT DEFAULT NULL, status VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_C7056A994584665A ON sylius_product_review (product_id)'); + $this->addSql('CREATE INDEX IDX_C7056A99F675F31B ON sylius_product_review (author_id)'); + $this->addSql('CREATE TABLE sylius_product_taxon (id INT NOT NULL, product_id INT NOT NULL, taxon_id INT NOT NULL, position INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_169C6CD94584665A ON sylius_product_taxon (product_id)'); + $this->addSql('CREATE INDEX IDX_169C6CD9DE13F470 ON sylius_product_taxon (taxon_id)'); + $this->addSql('CREATE UNIQUE INDEX product_taxon_idx ON sylius_product_taxon (product_id, taxon_id)'); + $this->addSql('CREATE TABLE sylius_product_translation (id INT NOT NULL, translatable_id INT NOT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, meta_keywords VARCHAR(255) DEFAULT NULL, meta_description VARCHAR(255) DEFAULT NULL, short_description TEXT DEFAULT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_105A9082C2AC5D3 ON sylius_product_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_105A9084180C698989D9B62 ON sylius_product_translation (locale, slug)'); + $this->addSql('CREATE UNIQUE INDEX sylius_product_translation_uniq_trans ON sylius_product_translation (translatable_id, locale)'); + $this->addSql('CREATE TABLE sylius_product_variant (id INT NOT NULL, product_id INT NOT NULL, tax_category_id INT DEFAULT NULL, shipping_category_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, position INT NOT NULL, enabled BOOLEAN NOT NULL, version INT DEFAULT 1 NOT NULL, on_hold INT NOT NULL, on_hand INT NOT NULL, tracked BOOLEAN NOT NULL, width DOUBLE PRECISION DEFAULT NULL, height DOUBLE PRECISION DEFAULT NULL, depth DOUBLE PRECISION DEFAULT NULL, weight DOUBLE PRECISION DEFAULT NULL, shipping_required BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_A29B52377153098 ON sylius_product_variant (code)'); + $this->addSql('CREATE INDEX IDX_A29B5234584665A ON sylius_product_variant (product_id)'); + $this->addSql('CREATE INDEX IDX_A29B5239DF894ED ON sylius_product_variant (tax_category_id)'); + $this->addSql('CREATE INDEX IDX_A29B5239E2D1A41 ON sylius_product_variant (shipping_category_id)'); + $this->addSql('CREATE TABLE sylius_product_variant_option_value (variant_id INT NOT NULL, option_value_id INT NOT NULL, PRIMARY KEY(variant_id, option_value_id))'); + $this->addSql('CREATE INDEX IDX_76CDAFA13B69A9AF ON sylius_product_variant_option_value (variant_id)'); + $this->addSql('CREATE INDEX IDX_76CDAFA1D957CA06 ON sylius_product_variant_option_value (option_value_id)'); + $this->addSql('CREATE TABLE sylius_product_variant_translation (id INT NOT NULL, translatable_id INT NOT NULL, name VARCHAR(255) DEFAULT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_8DC18EDC2C2AC5D3 ON sylius_product_variant_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX sylius_product_variant_translation_uniq_trans ON sylius_product_variant_translation (translatable_id, locale)'); + $this->addSql('CREATE TABLE sylius_promotion (id INT NOT NULL, code VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, priority INT NOT NULL, exclusive BOOLEAN NOT NULL, usage_limit INT DEFAULT NULL, used INT NOT NULL, coupon_based BOOLEAN NOT NULL, starts_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, ends_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, applies_to_discounted BOOLEAN DEFAULT true NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_F157396377153098 ON sylius_promotion (code)'); + $this->addSql('CREATE TABLE sylius_promotion_channels (promotion_id INT NOT NULL, channel_id INT NOT NULL, PRIMARY KEY(promotion_id, channel_id))'); + $this->addSql('CREATE INDEX IDX_1A044F64139DF194 ON sylius_promotion_channels (promotion_id)'); + $this->addSql('CREATE INDEX IDX_1A044F6472F5A1AA ON sylius_promotion_channels (channel_id)'); + $this->addSql('CREATE TABLE sylius_promotion_action (id INT NOT NULL, promotion_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, configuration TEXT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_933D0915139DF194 ON sylius_promotion_action (promotion_id)'); + $this->addSql('COMMENT ON COLUMN sylius_promotion_action.configuration IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE sylius_promotion_coupon (id INT NOT NULL, promotion_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, usage_limit INT DEFAULT NULL, used INT NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, per_customer_usage_limit INT DEFAULT NULL, reusable_from_cancelled_orders BOOLEAN DEFAULT true NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B04EBA8577153098 ON sylius_promotion_coupon (code)'); + $this->addSql('CREATE INDEX IDX_B04EBA85139DF194 ON sylius_promotion_coupon (promotion_id)'); + $this->addSql('CREATE TABLE sylius_promotion_rule (id INT NOT NULL, promotion_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, configuration TEXT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_2C188EA8139DF194 ON sylius_promotion_rule (promotion_id)'); + $this->addSql('COMMENT ON COLUMN sylius_promotion_rule.configuration IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE sylius_province (id INT NOT NULL, country_id INT NOT NULL, code VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, abbreviation VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B5618FE477153098 ON sylius_province (code)'); + $this->addSql('CREATE INDEX IDX_B5618FE4F92F3E70 ON sylius_province (country_id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B5618FE4F92F3E705E237E06 ON sylius_province (country_id, name)'); + $this->addSql('CREATE TABLE sylius_shipment (id INT NOT NULL, method_id INT NOT NULL, order_id INT NOT NULL, state VARCHAR(255) NOT NULL, tracking VARCHAR(255) DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, shipped_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, adjustments_total INT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_FD707B3319883967 ON sylius_shipment (method_id)'); + $this->addSql('CREATE INDEX IDX_FD707B338D9F6D38 ON sylius_shipment (order_id)'); + $this->addSql('CREATE TABLE sylius_shipping_category (id INT NOT NULL, code VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_B1D6465277153098 ON sylius_shipping_category (code)'); + $this->addSql('CREATE TABLE sylius_shipping_method (id INT NOT NULL, category_id INT DEFAULT NULL, zone_id INT NOT NULL, tax_category_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, configuration TEXT NOT NULL, category_requirement INT NOT NULL, calculator VARCHAR(255) NOT NULL, is_enabled BOOLEAN NOT NULL, position INT NOT NULL, archived_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_5FB0EE1177153098 ON sylius_shipping_method (code)'); + $this->addSql('CREATE INDEX IDX_5FB0EE1112469DE2 ON sylius_shipping_method (category_id)'); + $this->addSql('CREATE INDEX IDX_5FB0EE119F2C3FAB ON sylius_shipping_method (zone_id)'); + $this->addSql('CREATE INDEX IDX_5FB0EE119DF894ED ON sylius_shipping_method (tax_category_id)'); + $this->addSql('COMMENT ON COLUMN sylius_shipping_method.configuration IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE sylius_shipping_method_channels (shipping_method_id INT NOT NULL, channel_id INT NOT NULL, PRIMARY KEY(shipping_method_id, channel_id))'); + $this->addSql('CREATE INDEX IDX_2D9833355F7D6850 ON sylius_shipping_method_channels (shipping_method_id)'); + $this->addSql('CREATE INDEX IDX_2D98333572F5A1AA ON sylius_shipping_method_channels (channel_id)'); + $this->addSql('CREATE TABLE sylius_shipping_method_rule (id INT NOT NULL, shipping_method_id INT DEFAULT NULL, type VARCHAR(255) NOT NULL, configuration TEXT NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_88A0EB655F7D6850 ON sylius_shipping_method_rule (shipping_method_id)'); + $this->addSql('COMMENT ON COLUMN sylius_shipping_method_rule.configuration IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE sylius_shipping_method_translation (id INT NOT NULL, translatable_id INT NOT NULL, name VARCHAR(255) NOT NULL, description VARCHAR(255) DEFAULT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_2B37DB3D2C2AC5D3 ON sylius_shipping_method_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX sylius_shipping_method_translation_uniq_trans ON sylius_shipping_method_translation (translatable_id, locale)'); + $this->addSql('CREATE TABLE sylius_shop_billing_data (id INT NOT NULL, company VARCHAR(255) DEFAULT NULL, tax_id VARCHAR(255) DEFAULT NULL, country_code VARCHAR(255) DEFAULT NULL, street VARCHAR(255) DEFAULT NULL, city VARCHAR(255) DEFAULT NULL, postcode VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE sylius_shop_user (id INT NOT NULL, customer_id INT NOT NULL, username VARCHAR(255) DEFAULT NULL, username_canonical VARCHAR(255) DEFAULT NULL, enabled BOOLEAN NOT NULL, salt VARCHAR(255) NOT NULL, password VARCHAR(255) DEFAULT NULL, encoder_name VARCHAR(255) DEFAULT NULL, last_login TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, password_reset_token VARCHAR(255) DEFAULT NULL, password_requested_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, email_verification_token VARCHAR(255) DEFAULT NULL, verified_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, locked BOOLEAN NOT NULL, expires_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, credentials_expire_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, roles TEXT NOT NULL, email VARCHAR(255) DEFAULT NULL, email_canonical VARCHAR(255) DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7C2B74809395C3F3 ON sylius_shop_user (customer_id)'); + $this->addSql('COMMENT ON COLUMN sylius_shop_user.roles IS \'(DC2Type:array)\''); + $this->addSql('CREATE TABLE sylius_tax_category (id INT NOT NULL, code VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_221EB0BE77153098 ON sylius_tax_category (code)'); + $this->addSql('CREATE TABLE sylius_tax_rate (id INT NOT NULL, category_id INT NOT NULL, zone_id INT NOT NULL, code VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, amount NUMERIC(10, 5) NOT NULL, included_in_price BOOLEAN NOT NULL, calculator VARCHAR(255) NOT NULL, start_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, end_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_3CD86B2E77153098 ON sylius_tax_rate (code)'); + $this->addSql('CREATE INDEX IDX_3CD86B2E12469DE2 ON sylius_tax_rate (category_id)'); + $this->addSql('CREATE INDEX IDX_3CD86B2E9F2C3FAB ON sylius_tax_rate (zone_id)'); + $this->addSql('CREATE TABLE sylius_taxon (id INT NOT NULL, tree_root INT DEFAULT NULL, parent_id INT DEFAULT NULL, code VARCHAR(255) NOT NULL, tree_left INT NOT NULL, tree_right INT NOT NULL, tree_level INT NOT NULL, position INT NOT NULL, enabled BOOLEAN NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_CFD811CA77153098 ON sylius_taxon (code)'); + $this->addSql('CREATE INDEX IDX_CFD811CAA977936C ON sylius_taxon (tree_root)'); + $this->addSql('CREATE INDEX IDX_CFD811CA727ACA70 ON sylius_taxon (parent_id)'); + $this->addSql('CREATE TABLE sylius_taxon_image (id INT NOT NULL, owner_id INT NOT NULL, type VARCHAR(255) DEFAULT NULL, path VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_DBE52B287E3C61F9 ON sylius_taxon_image (owner_id)'); + $this->addSql('CREATE TABLE sylius_taxon_translation (id INT NOT NULL, translatable_id INT NOT NULL, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, description TEXT DEFAULT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_1487DFCF2C2AC5D3 ON sylius_taxon_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX slug_uidx ON sylius_taxon_translation (locale, slug)'); + $this->addSql('CREATE UNIQUE INDEX sylius_taxon_translation_uniq_trans ON sylius_taxon_translation (translatable_id, locale)'); + $this->addSql('CREATE TABLE sylius_user_oauth (id INT NOT NULL, user_id INT DEFAULT NULL, provider VARCHAR(255) NOT NULL, identifier VARCHAR(255) NOT NULL, access_token VARCHAR(255) DEFAULT NULL, refresh_token VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_C3471B78A76ED395 ON sylius_user_oauth (user_id)'); + $this->addSql('CREATE UNIQUE INDEX user_provider ON sylius_user_oauth (user_id, provider)'); + $this->addSql('CREATE TABLE sylius_zone (id INT NOT NULL, code VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, type VARCHAR(8) NOT NULL, scope VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7BE2258E77153098 ON sylius_zone (code)'); + $this->addSql('CREATE TABLE sylius_zone_member (id INT NOT NULL, belongs_to INT DEFAULT NULL, code VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_E8B5ABF34B0E929B ON sylius_zone_member (belongs_to)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_E8B5ABF34B0E929B77153098 ON sylius_zone_member (belongs_to, code)'); + $this->addSql('CREATE TABLE messenger_messages (id BIGSERIAL NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)'); + $this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)'); + $this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)'); + $this->addSql('CREATE OR REPLACE FUNCTION notify_messenger_messages() RETURNS TRIGGER AS $$ + BEGIN + PERFORM pg_notify(\'messenger_messages\', NEW.queue_name::text); + RETURN NEW; + END; + $$ LANGUAGE plpgsql;'); + $this->addSql('DROP TRIGGER IF EXISTS notify_trigger ON messenger_messages;'); + $this->addSql('CREATE TRIGGER notify_trigger AFTER INSERT OR UPDATE ON messenger_messages FOR EACH ROW EXECUTE PROCEDURE notify_messenger_messages();'); + $this->addSql('ALTER TABLE sylius_address ADD CONSTRAINT FK_B97FF0589395C3F3 FOREIGN KEY (customer_id) REFERENCES sylius_customer (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_adjustment ADD CONSTRAINT FK_ACA6E0F28D9F6D38 FOREIGN KEY (order_id) REFERENCES sylius_order (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_adjustment ADD CONSTRAINT FK_ACA6E0F2E415FB15 FOREIGN KEY (order_item_id) REFERENCES sylius_order_item (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_adjustment ADD CONSTRAINT FK_ACA6E0F2F720C233 FOREIGN KEY (order_item_unit_id) REFERENCES sylius_order_item_unit (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_adjustment ADD CONSTRAINT FK_ACA6E0F27BE036FC FOREIGN KEY (shipment_id) REFERENCES sylius_shipment (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_avatar_image ADD CONSTRAINT FK_1068A3A97E3C61F9 FOREIGN KEY (owner_id) REFERENCES sylius_admin_user (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_catalog_promotion_channels ADD CONSTRAINT FK_48E9AE7622E2CB5A FOREIGN KEY (catalog_promotion_id) REFERENCES sylius_catalog_promotion (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_catalog_promotion_channels ADD CONSTRAINT FK_48E9AE7672F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_catalog_promotion_action ADD CONSTRAINT FK_F529624722E2CB5A FOREIGN KEY (catalog_promotion_id) REFERENCES sylius_catalog_promotion (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_catalog_promotion_scope ADD CONSTRAINT FK_584AA86A139DF194 FOREIGN KEY (promotion_id) REFERENCES sylius_catalog_promotion (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_catalog_promotion_translation ADD CONSTRAINT FK_BA065D3C2C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_catalog_promotion (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel ADD CONSTRAINT FK_16C8119EB5282EDF FOREIGN KEY (shop_billing_data_id) REFERENCES sylius_shop_billing_data (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel ADD CONSTRAINT FK_16C8119E743BF776 FOREIGN KEY (default_locale_id) REFERENCES sylius_locale (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel ADD CONSTRAINT FK_16C8119E3101778E FOREIGN KEY (base_currency_id) REFERENCES sylius_currency (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel ADD CONSTRAINT FK_16C8119EA978C17 FOREIGN KEY (default_tax_zone_id) REFERENCES sylius_zone (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel ADD CONSTRAINT FK_16C8119EF242B1E6 FOREIGN KEY (menu_taxon_id) REFERENCES sylius_taxon (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_currencies ADD CONSTRAINT FK_AE491F9372F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_currencies ADD CONSTRAINT FK_AE491F9338248176 FOREIGN KEY (currency_id) REFERENCES sylius_currency (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_locales ADD CONSTRAINT FK_786B7A8472F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_locales ADD CONSTRAINT FK_786B7A84E559DFD1 FOREIGN KEY (locale_id) REFERENCES sylius_locale (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_countries ADD CONSTRAINT FK_D96E51AE72F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_countries ADD CONSTRAINT FK_D96E51AEF92F3E70 FOREIGN KEY (country_id) REFERENCES sylius_country (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_pricing ADD CONSTRAINT FK_7801820CA80EF684 FOREIGN KEY (product_variant_id) REFERENCES sylius_product_variant (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_pricing_catalog_promotions ADD CONSTRAINT FK_9F52FF513EADFFE5 FOREIGN KEY (channel_pricing_id) REFERENCES sylius_channel_pricing (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_pricing_catalog_promotions ADD CONSTRAINT FK_9F52FF5122E2CB5A FOREIGN KEY (catalog_promotion_id) REFERENCES sylius_catalog_promotion (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_customer ADD CONSTRAINT FK_7E82D5E6D2919A68 FOREIGN KEY (customer_group_id) REFERENCES sylius_customer_group (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_customer ADD CONSTRAINT FK_7E82D5E6BD94FB16 FOREIGN KEY (default_address_id) REFERENCES sylius_address (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_exchange_rate ADD CONSTRAINT FK_5F52B852A76BEED FOREIGN KEY (source_currency) REFERENCES sylius_currency (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_exchange_rate ADD CONSTRAINT FK_5F52B85B3FD5856 FOREIGN KEY (target_currency) REFERENCES sylius_currency (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_order ADD CONSTRAINT FK_6196A1F94D4CFF2B FOREIGN KEY (shipping_address_id) REFERENCES sylius_address (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_order ADD CONSTRAINT FK_6196A1F979D0C0E4 FOREIGN KEY (billing_address_id) REFERENCES sylius_address (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_order ADD CONSTRAINT FK_6196A1F972F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_order ADD CONSTRAINT FK_6196A1F917B24436 FOREIGN KEY (promotion_coupon_id) REFERENCES sylius_promotion_coupon (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_order ADD CONSTRAINT FK_6196A1F99395C3F3 FOREIGN KEY (customer_id) REFERENCES sylius_customer (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_promotion_order ADD CONSTRAINT FK_BF9CF6FB8D9F6D38 FOREIGN KEY (order_id) REFERENCES sylius_order (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_promotion_order ADD CONSTRAINT FK_BF9CF6FB139DF194 FOREIGN KEY (promotion_id) REFERENCES sylius_promotion (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_order_item ADD CONSTRAINT FK_77B587ED8D9F6D38 FOREIGN KEY (order_id) REFERENCES sylius_order (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_order_item ADD CONSTRAINT FK_77B587ED3B69A9AF FOREIGN KEY (variant_id) REFERENCES sylius_product_variant (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_order_item_unit ADD CONSTRAINT FK_82BF226EE415FB15 FOREIGN KEY (order_item_id) REFERENCES sylius_order_item (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_order_item_unit ADD CONSTRAINT FK_82BF226E7BE036FC FOREIGN KEY (shipment_id) REFERENCES sylius_shipment (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_payment ADD CONSTRAINT FK_D9191BD419883967 FOREIGN KEY (method_id) REFERENCES sylius_payment_method (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_payment ADD CONSTRAINT FK_D9191BD48D9F6D38 FOREIGN KEY (order_id) REFERENCES sylius_order (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_payment_method ADD CONSTRAINT FK_A75B0B0DF23D6140 FOREIGN KEY (gateway_config_id) REFERENCES sylius_gateway_config (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_payment_method_channels ADD CONSTRAINT FK_543AC0CC5AA1164F FOREIGN KEY (payment_method_id) REFERENCES sylius_payment_method (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_payment_method_channels ADD CONSTRAINT FK_543AC0CC72F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_payment_method_translation ADD CONSTRAINT FK_966BE3A12C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_payment_method (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product ADD CONSTRAINT FK_677B9B74731E505 FOREIGN KEY (main_taxon_id) REFERENCES sylius_taxon (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_channels ADD CONSTRAINT FK_F9EF269B4584665A FOREIGN KEY (product_id) REFERENCES sylius_product (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_channels ADD CONSTRAINT FK_F9EF269B72F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_options ADD CONSTRAINT FK_2B5FF0094584665A FOREIGN KEY (product_id) REFERENCES sylius_product (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_options ADD CONSTRAINT FK_2B5FF009A7C41D6F FOREIGN KEY (option_id) REFERENCES sylius_product_option (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_association ADD CONSTRAINT FK_48E9CDABB1E1C39 FOREIGN KEY (association_type_id) REFERENCES sylius_product_association_type (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_association ADD CONSTRAINT FK_48E9CDAB4584665A FOREIGN KEY (product_id) REFERENCES sylius_product (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_association_product ADD CONSTRAINT FK_A427B983EFB9C8A5 FOREIGN KEY (association_id) REFERENCES sylius_product_association (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_association_product ADD CONSTRAINT FK_A427B9834584665A FOREIGN KEY (product_id) REFERENCES sylius_product (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_association_type_translation ADD CONSTRAINT FK_4F618E52C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_product_association_type (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_attribute_translation ADD CONSTRAINT FK_93850EBA2C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_product_attribute (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_attribute_value ADD CONSTRAINT FK_8A053E544584665A FOREIGN KEY (product_id) REFERENCES sylius_product (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_attribute_value ADD CONSTRAINT FK_8A053E54B6E62EFA FOREIGN KEY (attribute_id) REFERENCES sylius_product_attribute (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_image ADD CONSTRAINT FK_88C64B2D7E3C61F9 FOREIGN KEY (owner_id) REFERENCES sylius_product (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_image_product_variants ADD CONSTRAINT FK_8FFDAE8D3DA5256D FOREIGN KEY (image_id) REFERENCES sylius_product_image (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_image_product_variants ADD CONSTRAINT FK_8FFDAE8D3B69A9AF FOREIGN KEY (variant_id) REFERENCES sylius_product_variant (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_option_translation ADD CONSTRAINT FK_CBA491AD2C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_product_option (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_option_value ADD CONSTRAINT FK_F7FF7D4BA7C41D6F FOREIGN KEY (option_id) REFERENCES sylius_product_option (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_option_value_translation ADD CONSTRAINT FK_8D4382DC2C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_product_option_value (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_review ADD CONSTRAINT FK_C7056A994584665A FOREIGN KEY (product_id) REFERENCES sylius_product (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_review ADD CONSTRAINT FK_C7056A99F675F31B FOREIGN KEY (author_id) REFERENCES sylius_customer (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_taxon ADD CONSTRAINT FK_169C6CD94584665A FOREIGN KEY (product_id) REFERENCES sylius_product (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_taxon ADD CONSTRAINT FK_169C6CD9DE13F470 FOREIGN KEY (taxon_id) REFERENCES sylius_taxon (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_translation ADD CONSTRAINT FK_105A9082C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_product (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_variant ADD CONSTRAINT FK_A29B5234584665A FOREIGN KEY (product_id) REFERENCES sylius_product (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_variant ADD CONSTRAINT FK_A29B5239DF894ED FOREIGN KEY (tax_category_id) REFERENCES sylius_tax_category (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_variant ADD CONSTRAINT FK_A29B5239E2D1A41 FOREIGN KEY (shipping_category_id) REFERENCES sylius_shipping_category (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_variant_option_value ADD CONSTRAINT FK_76CDAFA13B69A9AF FOREIGN KEY (variant_id) REFERENCES sylius_product_variant (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_variant_option_value ADD CONSTRAINT FK_76CDAFA1D957CA06 FOREIGN KEY (option_value_id) REFERENCES sylius_product_option_value (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_product_variant_translation ADD CONSTRAINT FK_8DC18EDC2C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_product_variant (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_promotion_channels ADD CONSTRAINT FK_1A044F64139DF194 FOREIGN KEY (promotion_id) REFERENCES sylius_promotion (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_promotion_channels ADD CONSTRAINT FK_1A044F6472F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_promotion_action ADD CONSTRAINT FK_933D0915139DF194 FOREIGN KEY (promotion_id) REFERENCES sylius_promotion (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_promotion_coupon ADD CONSTRAINT FK_B04EBA85139DF194 FOREIGN KEY (promotion_id) REFERENCES sylius_promotion (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_promotion_rule ADD CONSTRAINT FK_2C188EA8139DF194 FOREIGN KEY (promotion_id) REFERENCES sylius_promotion (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_province ADD CONSTRAINT FK_B5618FE4F92F3E70 FOREIGN KEY (country_id) REFERENCES sylius_country (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_shipment ADD CONSTRAINT FK_FD707B3319883967 FOREIGN KEY (method_id) REFERENCES sylius_shipping_method (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_shipment ADD CONSTRAINT FK_FD707B338D9F6D38 FOREIGN KEY (order_id) REFERENCES sylius_order (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_shipping_method ADD CONSTRAINT FK_5FB0EE1112469DE2 FOREIGN KEY (category_id) REFERENCES sylius_shipping_category (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_shipping_method ADD CONSTRAINT FK_5FB0EE119F2C3FAB FOREIGN KEY (zone_id) REFERENCES sylius_zone (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_shipping_method ADD CONSTRAINT FK_5FB0EE119DF894ED FOREIGN KEY (tax_category_id) REFERENCES sylius_tax_category (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_shipping_method_channels ADD CONSTRAINT FK_2D9833355F7D6850 FOREIGN KEY (shipping_method_id) REFERENCES sylius_shipping_method (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_shipping_method_channels ADD CONSTRAINT FK_2D98333572F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_shipping_method_rule ADD CONSTRAINT FK_88A0EB655F7D6850 FOREIGN KEY (shipping_method_id) REFERENCES sylius_shipping_method (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_shipping_method_translation ADD CONSTRAINT FK_2B37DB3D2C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_shipping_method (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_shop_user ADD CONSTRAINT FK_7C2B74809395C3F3 FOREIGN KEY (customer_id) REFERENCES sylius_customer (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_tax_rate ADD CONSTRAINT FK_3CD86B2E12469DE2 FOREIGN KEY (category_id) REFERENCES sylius_tax_category (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_tax_rate ADD CONSTRAINT FK_3CD86B2E9F2C3FAB FOREIGN KEY (zone_id) REFERENCES sylius_zone (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_taxon ADD CONSTRAINT FK_CFD811CAA977936C FOREIGN KEY (tree_root) REFERENCES sylius_taxon (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_taxon ADD CONSTRAINT FK_CFD811CA727ACA70 FOREIGN KEY (parent_id) REFERENCES sylius_taxon (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_taxon_image ADD CONSTRAINT FK_DBE52B287E3C61F9 FOREIGN KEY (owner_id) REFERENCES sylius_taxon (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_taxon_translation ADD CONSTRAINT FK_1487DFCF2C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_taxon (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_user_oauth ADD CONSTRAINT FK_C3471B78A76ED395 FOREIGN KEY (user_id) REFERENCES sylius_shop_user (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_zone_member ADD CONSTRAINT FK_E8B5ABF34B0E929B FOREIGN KEY (belongs_to) REFERENCES sylius_zone (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP SEQUENCE sylius_address_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_address_log_entries_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_adjustment_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_admin_user_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_avatar_image_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_catalog_promotion_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_catalog_promotion_action_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_catalog_promotion_scope_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_catalog_promotion_translation_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_channel_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_channel_pricing_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_country_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_currency_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_customer_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_customer_group_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_exchange_rate_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_gateway_config_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_locale_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_order_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_order_item_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_order_item_unit_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_order_sequence_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_payment_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_payment_method_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_payment_method_translation_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_association_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_association_type_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_association_type_translation_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_attribute_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_attribute_translation_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_attribute_value_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_image_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_option_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_option_translation_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_option_value_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_option_value_translation_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_review_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_taxon_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_translation_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_variant_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_product_variant_translation_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_promotion_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_promotion_action_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_promotion_coupon_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_promotion_rule_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_province_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_shipment_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_shipping_category_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_shipping_method_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_shipping_method_rule_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_shipping_method_translation_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_shop_billing_data_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_shop_user_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_tax_category_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_tax_rate_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_taxon_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_taxon_image_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_taxon_translation_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_user_oauth_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_zone_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_zone_member_id_seq CASCADE'); + $this->addSql('ALTER TABLE sylius_address DROP CONSTRAINT FK_B97FF0589395C3F3'); + $this->addSql('ALTER TABLE sylius_adjustment DROP CONSTRAINT FK_ACA6E0F28D9F6D38'); + $this->addSql('ALTER TABLE sylius_adjustment DROP CONSTRAINT FK_ACA6E0F2E415FB15'); + $this->addSql('ALTER TABLE sylius_adjustment DROP CONSTRAINT FK_ACA6E0F2F720C233'); + $this->addSql('ALTER TABLE sylius_adjustment DROP CONSTRAINT FK_ACA6E0F27BE036FC'); + $this->addSql('ALTER TABLE sylius_avatar_image DROP CONSTRAINT FK_1068A3A97E3C61F9'); + $this->addSql('ALTER TABLE sylius_catalog_promotion_channels DROP CONSTRAINT FK_48E9AE7622E2CB5A'); + $this->addSql('ALTER TABLE sylius_catalog_promotion_channels DROP CONSTRAINT FK_48E9AE7672F5A1AA'); + $this->addSql('ALTER TABLE sylius_catalog_promotion_action DROP CONSTRAINT FK_F529624722E2CB5A'); + $this->addSql('ALTER TABLE sylius_catalog_promotion_scope DROP CONSTRAINT FK_584AA86A139DF194'); + $this->addSql('ALTER TABLE sylius_catalog_promotion_translation DROP CONSTRAINT FK_BA065D3C2C2AC5D3'); + $this->addSql('ALTER TABLE sylius_channel DROP CONSTRAINT FK_16C8119EB5282EDF'); + $this->addSql('ALTER TABLE sylius_channel DROP CONSTRAINT FK_16C8119E743BF776'); + $this->addSql('ALTER TABLE sylius_channel DROP CONSTRAINT FK_16C8119E3101778E'); + $this->addSql('ALTER TABLE sylius_channel DROP CONSTRAINT FK_16C8119EA978C17'); + $this->addSql('ALTER TABLE sylius_channel DROP CONSTRAINT FK_16C8119EF242B1E6'); + $this->addSql('ALTER TABLE sylius_channel_currencies DROP CONSTRAINT FK_AE491F9372F5A1AA'); + $this->addSql('ALTER TABLE sylius_channel_currencies DROP CONSTRAINT FK_AE491F9338248176'); + $this->addSql('ALTER TABLE sylius_channel_locales DROP CONSTRAINT FK_786B7A8472F5A1AA'); + $this->addSql('ALTER TABLE sylius_channel_locales DROP CONSTRAINT FK_786B7A84E559DFD1'); + $this->addSql('ALTER TABLE sylius_channel_countries DROP CONSTRAINT FK_D96E51AE72F5A1AA'); + $this->addSql('ALTER TABLE sylius_channel_countries DROP CONSTRAINT FK_D96E51AEF92F3E70'); + $this->addSql('ALTER TABLE sylius_channel_pricing DROP CONSTRAINT FK_7801820CA80EF684'); + $this->addSql('ALTER TABLE sylius_channel_pricing_catalog_promotions DROP CONSTRAINT FK_9F52FF513EADFFE5'); + $this->addSql('ALTER TABLE sylius_channel_pricing_catalog_promotions DROP CONSTRAINT FK_9F52FF5122E2CB5A'); + $this->addSql('ALTER TABLE sylius_customer DROP CONSTRAINT FK_7E82D5E6D2919A68'); + $this->addSql('ALTER TABLE sylius_customer DROP CONSTRAINT FK_7E82D5E6BD94FB16'); + $this->addSql('ALTER TABLE sylius_exchange_rate DROP CONSTRAINT FK_5F52B852A76BEED'); + $this->addSql('ALTER TABLE sylius_exchange_rate DROP CONSTRAINT FK_5F52B85B3FD5856'); + $this->addSql('ALTER TABLE sylius_order DROP CONSTRAINT FK_6196A1F94D4CFF2B'); + $this->addSql('ALTER TABLE sylius_order DROP CONSTRAINT FK_6196A1F979D0C0E4'); + $this->addSql('ALTER TABLE sylius_order DROP CONSTRAINT FK_6196A1F972F5A1AA'); + $this->addSql('ALTER TABLE sylius_order DROP CONSTRAINT FK_6196A1F917B24436'); + $this->addSql('ALTER TABLE sylius_order DROP CONSTRAINT FK_6196A1F99395C3F3'); + $this->addSql('ALTER TABLE sylius_promotion_order DROP CONSTRAINT FK_BF9CF6FB8D9F6D38'); + $this->addSql('ALTER TABLE sylius_promotion_order DROP CONSTRAINT FK_BF9CF6FB139DF194'); + $this->addSql('ALTER TABLE sylius_order_item DROP CONSTRAINT FK_77B587ED8D9F6D38'); + $this->addSql('ALTER TABLE sylius_order_item DROP CONSTRAINT FK_77B587ED3B69A9AF'); + $this->addSql('ALTER TABLE sylius_order_item_unit DROP CONSTRAINT FK_82BF226EE415FB15'); + $this->addSql('ALTER TABLE sylius_order_item_unit DROP CONSTRAINT FK_82BF226E7BE036FC'); + $this->addSql('ALTER TABLE sylius_payment DROP CONSTRAINT FK_D9191BD419883967'); + $this->addSql('ALTER TABLE sylius_payment DROP CONSTRAINT FK_D9191BD48D9F6D38'); + $this->addSql('ALTER TABLE sylius_payment_method DROP CONSTRAINT FK_A75B0B0DF23D6140'); + $this->addSql('ALTER TABLE sylius_payment_method_channels DROP CONSTRAINT FK_543AC0CC5AA1164F'); + $this->addSql('ALTER TABLE sylius_payment_method_channels DROP CONSTRAINT FK_543AC0CC72F5A1AA'); + $this->addSql('ALTER TABLE sylius_payment_method_translation DROP CONSTRAINT FK_966BE3A12C2AC5D3'); + $this->addSql('ALTER TABLE sylius_product DROP CONSTRAINT FK_677B9B74731E505'); + $this->addSql('ALTER TABLE sylius_product_channels DROP CONSTRAINT FK_F9EF269B4584665A'); + $this->addSql('ALTER TABLE sylius_product_channels DROP CONSTRAINT FK_F9EF269B72F5A1AA'); + $this->addSql('ALTER TABLE sylius_product_options DROP CONSTRAINT FK_2B5FF0094584665A'); + $this->addSql('ALTER TABLE sylius_product_options DROP CONSTRAINT FK_2B5FF009A7C41D6F'); + $this->addSql('ALTER TABLE sylius_product_association DROP CONSTRAINT FK_48E9CDABB1E1C39'); + $this->addSql('ALTER TABLE sylius_product_association DROP CONSTRAINT FK_48E9CDAB4584665A'); + $this->addSql('ALTER TABLE sylius_product_association_product DROP CONSTRAINT FK_A427B983EFB9C8A5'); + $this->addSql('ALTER TABLE sylius_product_association_product DROP CONSTRAINT FK_A427B9834584665A'); + $this->addSql('ALTER TABLE sylius_product_association_type_translation DROP CONSTRAINT FK_4F618E52C2AC5D3'); + $this->addSql('ALTER TABLE sylius_product_attribute_translation DROP CONSTRAINT FK_93850EBA2C2AC5D3'); + $this->addSql('ALTER TABLE sylius_product_attribute_value DROP CONSTRAINT FK_8A053E544584665A'); + $this->addSql('ALTER TABLE sylius_product_attribute_value DROP CONSTRAINT FK_8A053E54B6E62EFA'); + $this->addSql('ALTER TABLE sylius_product_image DROP CONSTRAINT FK_88C64B2D7E3C61F9'); + $this->addSql('ALTER TABLE sylius_product_image_product_variants DROP CONSTRAINT FK_8FFDAE8D3DA5256D'); + $this->addSql('ALTER TABLE sylius_product_image_product_variants DROP CONSTRAINT FK_8FFDAE8D3B69A9AF'); + $this->addSql('ALTER TABLE sylius_product_option_translation DROP CONSTRAINT FK_CBA491AD2C2AC5D3'); + $this->addSql('ALTER TABLE sylius_product_option_value DROP CONSTRAINT FK_F7FF7D4BA7C41D6F'); + $this->addSql('ALTER TABLE sylius_product_option_value_translation DROP CONSTRAINT FK_8D4382DC2C2AC5D3'); + $this->addSql('ALTER TABLE sylius_product_review DROP CONSTRAINT FK_C7056A994584665A'); + $this->addSql('ALTER TABLE sylius_product_review DROP CONSTRAINT FK_C7056A99F675F31B'); + $this->addSql('ALTER TABLE sylius_product_taxon DROP CONSTRAINT FK_169C6CD94584665A'); + $this->addSql('ALTER TABLE sylius_product_taxon DROP CONSTRAINT FK_169C6CD9DE13F470'); + $this->addSql('ALTER TABLE sylius_product_translation DROP CONSTRAINT FK_105A9082C2AC5D3'); + $this->addSql('ALTER TABLE sylius_product_variant DROP CONSTRAINT FK_A29B5234584665A'); + $this->addSql('ALTER TABLE sylius_product_variant DROP CONSTRAINT FK_A29B5239DF894ED'); + $this->addSql('ALTER TABLE sylius_product_variant DROP CONSTRAINT FK_A29B5239E2D1A41'); + $this->addSql('ALTER TABLE sylius_product_variant_option_value DROP CONSTRAINT FK_76CDAFA13B69A9AF'); + $this->addSql('ALTER TABLE sylius_product_variant_option_value DROP CONSTRAINT FK_76CDAFA1D957CA06'); + $this->addSql('ALTER TABLE sylius_product_variant_translation DROP CONSTRAINT FK_8DC18EDC2C2AC5D3'); + $this->addSql('ALTER TABLE sylius_promotion_channels DROP CONSTRAINT FK_1A044F64139DF194'); + $this->addSql('ALTER TABLE sylius_promotion_channels DROP CONSTRAINT FK_1A044F6472F5A1AA'); + $this->addSql('ALTER TABLE sylius_promotion_action DROP CONSTRAINT FK_933D0915139DF194'); + $this->addSql('ALTER TABLE sylius_promotion_coupon DROP CONSTRAINT FK_B04EBA85139DF194'); + $this->addSql('ALTER TABLE sylius_promotion_rule DROP CONSTRAINT FK_2C188EA8139DF194'); + $this->addSql('ALTER TABLE sylius_province DROP CONSTRAINT FK_B5618FE4F92F3E70'); + $this->addSql('ALTER TABLE sylius_shipment DROP CONSTRAINT FK_FD707B3319883967'); + $this->addSql('ALTER TABLE sylius_shipment DROP CONSTRAINT FK_FD707B338D9F6D38'); + $this->addSql('ALTER TABLE sylius_shipping_method DROP CONSTRAINT FK_5FB0EE1112469DE2'); + $this->addSql('ALTER TABLE sylius_shipping_method DROP CONSTRAINT FK_5FB0EE119F2C3FAB'); + $this->addSql('ALTER TABLE sylius_shipping_method DROP CONSTRAINT FK_5FB0EE119DF894ED'); + $this->addSql('ALTER TABLE sylius_shipping_method_channels DROP CONSTRAINT FK_2D9833355F7D6850'); + $this->addSql('ALTER TABLE sylius_shipping_method_channels DROP CONSTRAINT FK_2D98333572F5A1AA'); + $this->addSql('ALTER TABLE sylius_shipping_method_rule DROP CONSTRAINT FK_88A0EB655F7D6850'); + $this->addSql('ALTER TABLE sylius_shipping_method_translation DROP CONSTRAINT FK_2B37DB3D2C2AC5D3'); + $this->addSql('ALTER TABLE sylius_shop_user DROP CONSTRAINT FK_7C2B74809395C3F3'); + $this->addSql('ALTER TABLE sylius_tax_rate DROP CONSTRAINT FK_3CD86B2E12469DE2'); + $this->addSql('ALTER TABLE sylius_tax_rate DROP CONSTRAINT FK_3CD86B2E9F2C3FAB'); + $this->addSql('ALTER TABLE sylius_taxon DROP CONSTRAINT FK_CFD811CAA977936C'); + $this->addSql('ALTER TABLE sylius_taxon DROP CONSTRAINT FK_CFD811CA727ACA70'); + $this->addSql('ALTER TABLE sylius_taxon_image DROP CONSTRAINT FK_DBE52B287E3C61F9'); + $this->addSql('ALTER TABLE sylius_taxon_translation DROP CONSTRAINT FK_1487DFCF2C2AC5D3'); + $this->addSql('ALTER TABLE sylius_user_oauth DROP CONSTRAINT FK_C3471B78A76ED395'); + $this->addSql('ALTER TABLE sylius_zone_member DROP CONSTRAINT FK_E8B5ABF34B0E929B'); + $this->addSql('DROP TABLE sylius_address'); + $this->addSql('DROP TABLE sylius_address_log_entries'); + $this->addSql('DROP TABLE sylius_adjustment'); + $this->addSql('DROP TABLE sylius_admin_user'); + $this->addSql('DROP TABLE sylius_avatar_image'); + $this->addSql('DROP TABLE sylius_catalog_promotion'); + $this->addSql('DROP TABLE sylius_catalog_promotion_channels'); + $this->addSql('DROP TABLE sylius_catalog_promotion_action'); + $this->addSql('DROP TABLE sylius_catalog_promotion_scope'); + $this->addSql('DROP TABLE sylius_catalog_promotion_translation'); + $this->addSql('DROP TABLE sylius_channel'); + $this->addSql('DROP TABLE sylius_channel_currencies'); + $this->addSql('DROP TABLE sylius_channel_locales'); + $this->addSql('DROP TABLE sylius_channel_countries'); + $this->addSql('DROP TABLE sylius_channel_pricing'); + $this->addSql('DROP TABLE sylius_channel_pricing_catalog_promotions'); + $this->addSql('DROP TABLE sylius_country'); + $this->addSql('DROP TABLE sylius_currency'); + $this->addSql('DROP TABLE sylius_customer'); + $this->addSql('DROP TABLE sylius_customer_group'); + $this->addSql('DROP TABLE sylius_exchange_rate'); + $this->addSql('DROP TABLE sylius_gateway_config'); + $this->addSql('DROP TABLE sylius_locale'); + $this->addSql('DROP TABLE sylius_order'); + $this->addSql('DROP TABLE sylius_promotion_order'); + $this->addSql('DROP TABLE sylius_order_item'); + $this->addSql('DROP TABLE sylius_order_item_unit'); + $this->addSql('DROP TABLE sylius_order_sequence'); + $this->addSql('DROP TABLE sylius_payment'); + $this->addSql('DROP TABLE sylius_payment_method'); + $this->addSql('DROP TABLE sylius_payment_method_channels'); + $this->addSql('DROP TABLE sylius_payment_method_translation'); + $this->addSql('DROP TABLE sylius_payment_security_token'); + $this->addSql('DROP TABLE sylius_product'); + $this->addSql('DROP TABLE sylius_product_channels'); + $this->addSql('DROP TABLE sylius_product_options'); + $this->addSql('DROP TABLE sylius_product_association'); + $this->addSql('DROP TABLE sylius_product_association_product'); + $this->addSql('DROP TABLE sylius_product_association_type'); + $this->addSql('DROP TABLE sylius_product_association_type_translation'); + $this->addSql('DROP TABLE sylius_product_attribute'); + $this->addSql('DROP TABLE sylius_product_attribute_translation'); + $this->addSql('DROP TABLE sylius_product_attribute_value'); + $this->addSql('DROP TABLE sylius_product_image'); + $this->addSql('DROP TABLE sylius_product_image_product_variants'); + $this->addSql('DROP TABLE sylius_product_option'); + $this->addSql('DROP TABLE sylius_product_option_translation'); + $this->addSql('DROP TABLE sylius_product_option_value'); + $this->addSql('DROP TABLE sylius_product_option_value_translation'); + $this->addSql('DROP TABLE sylius_product_review'); + $this->addSql('DROP TABLE sylius_product_taxon'); + $this->addSql('DROP TABLE sylius_product_translation'); + $this->addSql('DROP TABLE sylius_product_variant'); + $this->addSql('DROP TABLE sylius_product_variant_option_value'); + $this->addSql('DROP TABLE sylius_product_variant_translation'); + $this->addSql('DROP TABLE sylius_promotion'); + $this->addSql('DROP TABLE sylius_promotion_channels'); + $this->addSql('DROP TABLE sylius_promotion_action'); + $this->addSql('DROP TABLE sylius_promotion_coupon'); + $this->addSql('DROP TABLE sylius_promotion_rule'); + $this->addSql('DROP TABLE sylius_province'); + $this->addSql('DROP TABLE sylius_shipment'); + $this->addSql('DROP TABLE sylius_shipping_category'); + $this->addSql('DROP TABLE sylius_shipping_method'); + $this->addSql('DROP TABLE sylius_shipping_method_channels'); + $this->addSql('DROP TABLE sylius_shipping_method_rule'); + $this->addSql('DROP TABLE sylius_shipping_method_translation'); + $this->addSql('DROP TABLE sylius_shop_billing_data'); + $this->addSql('DROP TABLE sylius_shop_user'); + $this->addSql('DROP TABLE sylius_tax_category'); + $this->addSql('DROP TABLE sylius_tax_rate'); + $this->addSql('DROP TABLE sylius_taxon'); + $this->addSql('DROP TABLE sylius_taxon_image'); + $this->addSql('DROP TABLE sylius_taxon_translation'); + $this->addSql('DROP TABLE sylius_user_oauth'); + $this->addSql('DROP TABLE sylius_zone'); + $this->addSql('DROP TABLE sylius_zone_member'); + $this->addSql('DROP TABLE messenger_messages'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20230420151332.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230420151332.php new file mode 100644 index 0000000000..5ca4cb176f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230420151332.php @@ -0,0 +1,72 @@ +addSql('CREATE SEQUENCE sylius_channel_price_history_config_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_channel_pricing_log_entry_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE sylius_promotion_translation_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE sylius_channel_price_history_config (id INT NOT NULL, lowest_price_for_discounted_products_checking_period INT DEFAULT 30 NOT NULL, lowest_price_for_discounted_products_visible BOOLEAN DEFAULT true NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE sylius_channel_price_history_config_excluded_taxons (channel_id INT NOT NULL, taxon_id INT NOT NULL, PRIMARY KEY(channel_id, taxon_id))'); + $this->addSql('CREATE INDEX IDX_77FD02A72F5A1AA ON sylius_channel_price_history_config_excluded_taxons (channel_id)'); + $this->addSql('CREATE INDEX IDX_77FD02ADE13F470 ON sylius_channel_price_history_config_excluded_taxons (taxon_id)'); + $this->addSql('CREATE TABLE sylius_channel_pricing_log_entry (id INT NOT NULL, channel_pricing_id INT NOT NULL, price INT NOT NULL, original_price INT DEFAULT NULL, logged_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_77181A53EADFFE5 ON sylius_channel_pricing_log_entry (channel_pricing_id)'); + $this->addSql('CREATE TABLE sylius_promotion_translation (id INT NOT NULL, translatable_id INT NOT NULL, label VARCHAR(255) DEFAULT NULL, locale VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_3C7A76182C2AC5D3 ON sylius_promotion_translation (translatable_id)'); + $this->addSql('CREATE UNIQUE INDEX sylius_promotion_translation_uniq_trans ON sylius_promotion_translation (translatable_id, locale)'); + $this->addSql('ALTER TABLE sylius_channel_price_history_config_excluded_taxons ADD CONSTRAINT FK_77FD02A72F5A1AA FOREIGN KEY (channel_id) REFERENCES sylius_channel_price_history_config (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_price_history_config_excluded_taxons ADD CONSTRAINT FK_77FD02ADE13F470 FOREIGN KEY (taxon_id) REFERENCES sylius_taxon (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel_pricing_log_entry ADD CONSTRAINT FK_77181A53EADFFE5 FOREIGN KEY (channel_pricing_id) REFERENCES sylius_channel_pricing (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_promotion_translation ADD CONSTRAINT FK_3C7A76182C2AC5D3 FOREIGN KEY (translatable_id) REFERENCES sylius_promotion (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE sylius_channel ADD channel_price_history_config_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE sylius_channel ADD CONSTRAINT FK_16C8119E75F20EAE FOREIGN KEY (channel_price_history_config_id) REFERENCES sylius_channel_price_history_config (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_16C8119E75F20EAE ON sylius_channel (channel_price_history_config_id)'); + $this->addSql('ALTER TABLE sylius_channel_pricing ADD lowest_price_before_discount INT DEFAULT NULL'); + $this->addSql('ALTER TABLE sylius_user_oauth ALTER access_token TYPE TEXT'); + $this->addSql('ALTER TABLE sylius_user_oauth ALTER refresh_token TYPE TEXT'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_channel DROP CONSTRAINT FK_16C8119E75F20EAE'); + $this->addSql('DROP SEQUENCE sylius_channel_price_history_config_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_channel_pricing_log_entry_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE sylius_promotion_translation_id_seq CASCADE'); + $this->addSql('ALTER TABLE sylius_channel_price_history_config_excluded_taxons DROP CONSTRAINT FK_77FD02A72F5A1AA'); + $this->addSql('ALTER TABLE sylius_channel_price_history_config_excluded_taxons DROP CONSTRAINT FK_77FD02ADE13F470'); + $this->addSql('ALTER TABLE sylius_channel_pricing_log_entry DROP CONSTRAINT FK_77181A53EADFFE5'); + $this->addSql('ALTER TABLE sylius_promotion_translation DROP CONSTRAINT FK_3C7A76182C2AC5D3'); + $this->addSql('DROP TABLE sylius_channel_price_history_config'); + $this->addSql('DROP TABLE sylius_channel_price_history_config_excluded_taxons'); + $this->addSql('DROP TABLE sylius_channel_pricing_log_entry'); + $this->addSql('DROP TABLE sylius_promotion_translation'); + $this->addSql('DROP INDEX UNIQ_16C8119E75F20EAE'); + $this->addSql('ALTER TABLE sylius_channel DROP channel_price_history_config_id'); + $this->addSql('ALTER TABLE sylius_user_oauth ALTER access_token TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE sylius_user_oauth ALTER refresh_token TYPE VARCHAR(255)'); + $this->addSql('ALTER TABLE sylius_channel_pricing DROP lowest_price_before_discount'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20230426153930.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230426153930.php new file mode 100644 index 0000000000..b846e87874 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230426153930.php @@ -0,0 +1,37 @@ +addSql('ALTER TABLE sylius_product DROP CONSTRAINT FK_677B9B74731E505'); + $this->addSql('ALTER TABLE sylius_product ADD CONSTRAINT FK_677B9B74731E505 FOREIGN KEY (main_taxon_id) REFERENCES sylius_taxon (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_product DROP CONSTRAINT fk_677b9b74731e505'); + $this->addSql('ALTER TABLE sylius_product ADD CONSTRAINT fk_677b9b74731e505 FOREIGN KEY (main_taxon_id) REFERENCES sylius_taxon (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20230426154358.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230426154358.php new file mode 100644 index 0000000000..de9d2742be --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20230426154358.php @@ -0,0 +1,37 @@ +addSql('ALTER TABLE sylius_product DROP FOREIGN KEY FK_677B9B74731E505'); + $this->addSql('ALTER TABLE sylius_product ADD CONSTRAINT FK_677B9B74731E505 FOREIGN KEY (main_taxon_id) REFERENCES sylius_taxon (id) ON DELETE SET NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_product DROP FOREIGN KEY FK_677B9B74731E505'); + $this->addSql('ALTER TABLE sylius_product ADD CONSTRAINT FK_677B9B74731E505 FOREIGN KEY (main_taxon_id) REFERENCES sylius_taxon (id) ON UPDATE NO ACTION ON DELETE NO ACTION'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20231103004216.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20231103004216.php new file mode 100644 index 0000000000..4be36ec16a --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20231103004216.php @@ -0,0 +1,41 @@ +addSql('CREATE UNIQUE INDEX UNIQ_88D5CC4D6B7BA4B6 ON sylius_admin_user (password_reset_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_88D5CC4DC4995C67 ON sylius_admin_user (email_verification_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7C2B74806B7BA4B6 ON sylius_shop_user (password_reset_token)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_7C2B7480C4995C67 ON sylius_shop_user (email_verification_token)'); + } + + public function down(Schema $schema): void + { + $this->addSql('DROP INDEX UNIQ_88D5CC4D6B7BA4B6 ON sylius_admin_user'); + $this->addSql('DROP INDEX UNIQ_88D5CC4DC4995C67 ON sylius_admin_user'); + $this->addSql('DROP INDEX UNIQ_7C2B74806B7BA4B6 ON sylius_shop_user'); + $this->addSql('DROP INDEX UNIQ_7C2B7480C4995C67 ON sylius_shop_user'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110238.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110238.php new file mode 100644 index 0000000000..2cdbf13075 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110238.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE sylius_promotion ADD archived_at DATETIME DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_promotion DROP archived_at'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110239.php b/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110239.php new file mode 100644 index 0000000000..c145506596 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Migrations/Version20240216110239.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE sylius_promotion ADD archived_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE sylius_promotion DROP archived_at'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Order/Checker/OrderPromotionsIntegrityChecker.php b/src/Sylius/Bundle/CoreBundle/Order/Checker/OrderPromotionsIntegrityChecker.php index 8855883bc1..3434344c05 100644 --- a/src/Sylius/Bundle/CoreBundle/Order/Checker/OrderPromotionsIntegrityChecker.php +++ b/src/Sylius/Bundle/CoreBundle/Order/Checker/OrderPromotionsIntegrityChecker.php @@ -26,10 +26,12 @@ final class OrderPromotionsIntegrityChecker implements OrderPromotionsIntegrityC public function check(OrderInterface $order): ?PromotionInterface { + /** @var PromotionInterface[]|ArrayCollection $previousPromotions */ $previousPromotions = new ArrayCollection($order->getPromotions()->toArray()); $this->orderProcessor->process($order); + /** @var PromotionInterface $previousPromotion */ foreach ($previousPromotions as $previousPromotion) { if (!$order->getPromotions()->contains($previousPromotion)) { return $previousPromotion; diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/Command/ApplyLowestPriceOnChannelPricings.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/Command/ApplyLowestPriceOnChannelPricings.php new file mode 100644 index 0000000000..3191fa7a2a --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/Command/ApplyLowestPriceOnChannelPricings.php @@ -0,0 +1,21 @@ +batchSize; + $offset = 0; + + while ([] !== ($channelPricingsIds = $this->getIdsBatch($channel, $limit, $offset))) { + $this->commandBus->dispatch(new ApplyLowestPriceOnChannelPricings($channelPricingsIds)); + + $offset += $limit; + } + } + + private function getIdsBatch(ChannelInterface $channel, int $limit, int $offset): array + { + /** @var ChannelPricingInterface[] $channelPricings */ + $channelPricings = $this->channelPricingRepository->findBy( + ['channelCode' => $channel->getCode()], + ['id' => 'ASC'], + $limit, + $offset, + ); + + return (new ArrayCollection($channelPricings)) + ->map(fn (ChannelPricingInterface $channelPricing): mixed => $channelPricing->getId()) + ->getValues() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/CommandHandler/ApplyLowestPriceOnChannelPricingsHandler.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/CommandHandler/ApplyLowestPriceOnChannelPricingsHandler.php new file mode 100644 index 0000000000..59be192bbe --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/CommandHandler/ApplyLowestPriceOnChannelPricingsHandler.php @@ -0,0 +1,40 @@ +channelPricingRepository->findBy( + ['id' => $applyLowestPriceOnChannelPricings->channelPricingIds], + ); + + foreach ($channelPricings as $channelPricing) { + $this->productLowestPriceBeforeDiscountProcessor->process($channelPricing); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/Console/Command/ClearPriceHistoryCommand.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/Console/Command/ClearPriceHistoryCommand.php new file mode 100644 index 0000000000..e96e8ef0e3 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/Console/Command/ClearPriceHistoryCommand.php @@ -0,0 +1,71 @@ +addArgument('days', InputArgument::REQUIRED, 'Number of days ago'); + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $days = filter_var($input->getArgument('days'), \FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]); + if (false === $days) { + $this->io->error('Number of days must be an integer greater than 0'); + + return Command::FAILURE; + } + + if ($input->isInteractive()) { + $confirmation = $this->io->confirm(sprintf( + 'Are you sure you want to clear the price history from before %s days ago?', + $days, + ), false); + + if (false === $confirmation) { + return Command::INVALID; + } + } + + $this->channelPricingLogEntriesRemover->remove($days); + + return Command::SUCCESS; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/CreateLogEntryOnPriceChangeObserver.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/CreateLogEntryOnPriceChangeObserver.php new file mode 100644 index 0000000000..a46be5e3f8 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/CreateLogEntryOnPriceChangeObserver.php @@ -0,0 +1,42 @@ +priceChangeLogger->log($entity); + } + + public function supports(object $entity): bool + { + return $entity instanceof ChannelPricingInterface && null !== $entity->getPrice(); + } + + public function observedFields(): array + { + return ['price', 'originalPrice']; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/EntityObserverInterface.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/EntityObserverInterface.php new file mode 100644 index 0000000000..fa7580d296 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/EntityObserverInterface.php @@ -0,0 +1,26 @@ +channelsCurrentlyProcessed = [(string) $entity->getCode() => true]; + + $this->commandDispatcher->applyWithinChannel($entity); + + unset($this->channelsCurrentlyProcessed[(string) $entity->getCode()]); + } + + public function supports(object $entity): bool + { + return + $entity instanceof ChannelInterface && + !isset($this->channelsCurrentlyProcessed[$entity->getCode()]) && + $this->hasNewPriceHistoryConfig($entity) + ; + } + + public function observedFields(): array + { + return ['channelPriceHistoryConfig']; + } + + private function hasNewPriceHistoryConfig(ChannelInterface $channel): bool + { + return + (null !== $config = $channel->getChannelPriceHistoryConfig()) && + null === $config->getId() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserver.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserver.php new file mode 100644 index 0000000000..d3db5b9587 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserver.php @@ -0,0 +1,61 @@ +channelRepository->findOneBy(['channelPriceHistoryConfig' => $entity]); + if (null === $channel) { + return; + } + + $this->configsCurrentlyProcessed = [$entity->getId() => true]; + + $this->commandDispatcher->applyWithinChannel($channel); + + unset($this->configsCurrentlyProcessed[$entity->getId()]); + } + + public function supports(object $entity): bool + { + return + $entity instanceof ChannelPriceHistoryConfigInterface && + null !== $entity->getId() && + !isset($this->configsCurrentlyProcessed[$entity->getId()]) + ; + } + + public function observedFields(): array + { + return ['lowestPriceForDiscountedProductsCheckingPeriod']; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/Event/OldChannelPricingLogEntriesEvents.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/Event/OldChannelPricingLogEntriesEvents.php new file mode 100644 index 0000000000..afce2524e7 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/Event/OldChannelPricingLogEntriesEvents.php @@ -0,0 +1,21 @@ +getObject(); + + if (!$entity instanceof ChannelPricingLogEntryInterface) { + return; + } + + /** @var ChannelPricingInterface $channelPricing */ + $channelPricing = $entity->getChannelPricing(); + + $this->lowestPriceProcessor->process($channelPricing); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/EventListener/OnFlushEntityObserverListener.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/EventListener/OnFlushEntityObserverListener.php new file mode 100644 index 0000000000..539d1fcb44 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/EventListener/OnFlushEntityObserverListener.php @@ -0,0 +1,66 @@ +entityObservers, EntityObserverInterface::class); + } + + public function onFlush(OnFlushEventArgs $eventArgs): void + { + $entityManager = $eventArgs->getObjectManager(); + $unitOfWork = $entityManager->getUnitOfWork(); + + $scheduledEntities = array_merge( + $unitOfWork->getScheduledEntityInsertions(), + $unitOfWork->getScheduledEntityUpdates(), + ); + + $atLeastOneEntityChanged = false; + + foreach ($scheduledEntities as $entity) { + /** @var EntityObserverInterface $entityObserver */ + foreach ($this->entityObservers as $entityObserver) { + if ( + !$entityObserver->supports($entity) || + !$this->isEntityChanged($unitOfWork, $entity, $entityObserver->observedFields()) + ) { + continue; + } + + $atLeastOneEntityChanged = true; + $entityObserver->onChange($entity); + } + } + + if ($atLeastOneEntityChanged) { + $unitOfWork->computeChangeSets(); + } + } + + private function isEntityChanged(UnitOfWork $unitOfWork, object $entity, array $supportedFields): bool + { + $changedFields = array_keys($unitOfWork->getEntityChangeSet($entity)); + + return [] !== array_intersect($changedFields, $supportedFields); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/Logger/PriceChangeLogger.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/Logger/PriceChangeLogger.php new file mode 100644 index 0000000000..128dab9b3c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/Logger/PriceChangeLogger.php @@ -0,0 +1,44 @@ +getPrice()); + + $logEntry = $this->logEntryFactory->create( + $channelPricing, + $this->dateTimeProvider->now(), + $channelPricing->getPrice(), + $channelPricing->getOriginalPrice(), + ); + + $this->logEntryManager->persist($logEntry); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/Logger/PriceChangeLoggerInterface.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/Logger/PriceChangeLoggerInterface.php new file mode 100644 index 0000000000..62ab0716d8 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/Logger/PriceChangeLoggerInterface.php @@ -0,0 +1,21 @@ +isPromotionApplied($channelPricing)) { + $channelPricing->setLowestPriceBeforeDiscount(null); + + return; + } + + $latestLogEntry = $this->channelPricingLogEntryRepository->findLatestOneByChannelPricing($channelPricing); + + if ($latestLogEntry === null) { + $channelPricing->setLowestPriceBeforeDiscount(null); + + return; + } + + $channelCode = $channelPricing->getChannelCode(); + Assert::string($channelCode); + + /** @var ChannelInterface $channel */ + $channel = $this->channelRepository->findOneByCode($channelCode); + + if ( + !$channel instanceof ChannelInterface || + null === $channelPriceHistoryConfig = $channel->getChannelPriceHistoryConfig() + ) { + return; + } + + $lowestPriceInPeriod = $this->findLowestPriceInPeriod( + $latestLogEntry, + $channelPriceHistoryConfig->getLowestPriceForDiscountedProductsCheckingPeriod(), + ); + + $channelPricing->setLowestPriceBeforeDiscount($lowestPriceInPeriod); + } + + private function isPromotionApplied(ChannelPricingInterface $channelPricing): bool + { + return + $channelPricing->getOriginalPrice() !== null && + $channelPricing->getPrice() < $channelPricing->getOriginalPrice() + ; + } + + private function findLowestPriceInPeriod( + ChannelPricingLogEntryInterface $latestLogEntry, + int $lowestPriceForDiscountedProductsCheckingPeriod, + ): ?int { + $loggedAt = new \DateTimeImmutable($latestLogEntry->getLoggedAt()->format('Y-m-d H:i:s')); + + /** @var \DateTimeInterface $startDate */ + $startDate = $loggedAt->sub(new \DateInterval(sprintf('P%sD', $lowestPriceForDiscountedProductsCheckingPeriod))); + + return $this + ->channelPricingLogEntryRepository + ->findLowestPriceInPeriod($latestLogEntry->getId(), $latestLogEntry->getChannelPricing(), $startDate) + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/Processor/ProductLowestPriceBeforeDiscountProcessorInterface.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/Processor/ProductLowestPriceBeforeDiscountProcessorInterface.php new file mode 100644 index 0000000000..84d4eb7c34 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/Processor/ProductLowestPriceBeforeDiscountProcessorInterface.php @@ -0,0 +1,21 @@ +getFromDate($fromDays); + while ([] !== $oldChannelPricingLogEntries = $this->getBatch($fromDate)) { + foreach ($oldChannelPricingLogEntries as $oldChannelPricingLogEntry) { + $this->channelPricingLogEntriesManager->remove($oldChannelPricingLogEntry); + } + + $this->processDeletion($oldChannelPricingLogEntries); + } + } + + private function getBatch(\DateTimeInterface $fromDate): array + { + return $this->channelPricingLogEntriesRepository->findOlderThan($fromDate, $this->batchSize); + } + + private function processDeletion(array $deletedChannelPricingLogEntries): void + { + $this->eventDispatcher->dispatch(new GenericEvent($deletedChannelPricingLogEntries), OldChannelPricingLogEntriesEvents::PRE_REMOVE); + $this->channelPricingLogEntriesManager->flush(); + $this->eventDispatcher->dispatch(new GenericEvent($deletedChannelPricingLogEntries), OldChannelPricingLogEntriesEvents::POST_REMOVE); + $this->channelPricingLogEntriesManager->clear(); + } + + private function getFromDate(int $fromDays): \DateTimeInterface + { + $now = $this->dateTimeProvider->now(); + Assert::methodExists($now, 'modify'); + + return $now->modify(sprintf('-%d days', $fromDays)); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/PriceHistory/Remover/ChannelPricingLogEntriesRemoverInterface.php b/src/Sylius/Bundle/CoreBundle/PriceHistory/Remover/ChannelPricingLogEntriesRemoverInterface.php new file mode 100644 index 0000000000..fa89c4d504 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/PriceHistory/Remover/ChannelPricingLogEntriesRemoverInterface.php @@ -0,0 +1,19 @@ + - + @@ -56,7 +56,7 @@ - + @@ -66,7 +66,7 @@ - + @@ -83,5 +83,12 @@ + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ChannelPriceHistoryConfig.orm.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ChannelPriceHistoryConfig.orm.xml new file mode 100644 index 0000000000..31cf67170f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ChannelPriceHistoryConfig.orm.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ChannelPricing.orm.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ChannelPricing.orm.xml index cc048ed479..95803880fc 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ChannelPricing.orm.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ChannelPricing.orm.xml @@ -32,6 +32,7 @@ + @@ -50,6 +51,9 @@ + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ChannelPricingLogEntry.orm.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ChannelPricingLogEntry.orm.xml new file mode 100644 index 0000000000..95bd4097ee --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ChannelPricingLogEntry.orm.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Product.orm.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Product.orm.xml index 7d0ef2032c..9d81ccedfc 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Product.orm.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/Product.orm.xml @@ -35,6 +35,12 @@ + + + + + + @@ -51,7 +57,7 @@ - + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ProductReview.orm.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ProductReview.orm.xml index b42a3681ce..4dad179958 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ProductReview.orm.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/doctrine/model/ProductReview.orm.xml @@ -13,6 +13,17 @@ - + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/serialization/Messages/Admin/Account/RequestResetPasswordEmail.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/serialization/Messages/Admin/Account/RequestResetPasswordEmail.xml index 4b13a37dca..dae7bd8505 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/serialization/Messages/Admin/Account/RequestResetPasswordEmail.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/serialization/Messages/Admin/Account/RequestResetPasswordEmail.xml @@ -16,6 +16,7 @@ admin:reset_password:create + sylius:admin:reset_password:create diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/serialization/Messages/Admin/Account/ResetPassword.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/serialization/Messages/Admin/Account/ResetPassword.xml index a33c9c02e0..6a3a2497db 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/serialization/Messages/Admin/Account/ResetPassword.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/serialization/Messages/Admin/Account/ResetPassword.xml @@ -18,9 +18,11 @@ admin:reset_password:update + sylius:admin:reset_password:update admin:reset_password:update + sylius:admin:reset_password:update diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml index 7897b79e68..4a25536489 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services.xml @@ -13,38 +13,14 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + 9999 yes %env(bool:SYLIUS_UNSECURED_URLS)% + 100 @@ -142,7 +118,7 @@ - + @@ -152,6 +128,7 @@ order_items_based + @@ -164,6 +141,11 @@ + + + + + @@ -192,7 +174,12 @@ - + + + @@ -208,18 +195,27 @@ + + + + + + %sylius.channel_pricing_log_entry.old_logs_removal_batch_size% + + - + %sylius_order.order_expiration_period% + - + @@ -257,10 +253,6 @@ - - - - @@ -285,11 +277,23 @@ + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/catalog_promotion.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/catalog_promotion.xml index 24b6d05a63..1746779277 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/catalog_promotion.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/catalog_promotion.xml @@ -32,6 +32,7 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/catalog_promotion/processors.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/catalog_promotion/processors.xml index a0889da65c..a450eecba3 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/catalog_promotion/processors.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/catalog_promotion/processors.xml @@ -55,8 +55,7 @@ class="Sylius\Bundle\CoreBundle\CatalogPromotion\Processor\CatalogPromotionRemovalProcessor" > - - + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/commands.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/commands.xml deleted file mode 100644 index 291f28ecad..0000000000 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/commands.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/console_command.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/console_command.xml new file mode 100644 index 0000000000..8d77c90909 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/console_command.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/emails.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/emails.xml deleted file mode 100644 index f39759f3cc..0000000000 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/emails.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/fixtures.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/fixtures.xml index 69d95aa349..83384bb41d 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/fixtures.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/fixtures.xml @@ -199,7 +199,7 @@ - + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/fixtures_factories.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/fixtures_factories.xml index a9e2e6bf5a..41e373fee1 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/fixtures_factories.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/fixtures_factories.xml @@ -83,6 +83,7 @@ + @@ -120,7 +121,7 @@ - + @@ -177,9 +178,18 @@ - + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml index 98620c2b7f..6d30383539 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/form.xml @@ -51,6 +51,9 @@ sylius + + sylius + Sylius\Bundle\CoreBundle\CatalogPromotion\Calculator\FixedDiscountPriceCalculator::TYPE Sylius\Bundle\CoreBundle\CatalogPromotion\Calculator\PercentageDiscountPriceCalculator::TYPE Sylius\Bundle\CoreBundle\CatalogPromotion\Checker\InForProductScopeVariantChecker::TYPE @@ -69,7 +72,6 @@ - @@ -89,6 +91,7 @@ + @@ -110,6 +113,7 @@ + @@ -300,5 +304,14 @@ + + + + + + %sylius.model.channel_price_history_config.class% + %sylius.form.type.channel_price_history_config.validation_groups% + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/installer.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/installer.xml index d2b2a6886f..85cbdc0979 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/installer.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/installer.xml @@ -26,7 +26,7 @@ - + @@ -41,6 +41,7 @@ %locale% + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners.xml index 50fe67cdf5..22c4476fdf 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners.xml @@ -12,13 +12,18 @@ --> + + + + - + + @@ -26,6 +31,7 @@ + @@ -33,23 +39,28 @@ + + + + + @@ -59,6 +70,7 @@ + @@ -107,5 +119,9 @@ + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order.xml new file mode 100644 index 0000000000..b62bb8af50 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_checkout.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_checkout.xml new file mode 100644 index 0000000000..05d719706c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_checkout.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_payment.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_payment.xml new file mode 100644 index 0000000000..35b417a779 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_payment.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_shipping.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_shipping.xml new file mode 100644 index 0000000000..ed39c7d41d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/order_shipping.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/payment.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/payment.xml new file mode 100644 index 0000000000..45010ba4fe --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/payment.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/shipment.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/shipment.xml new file mode 100644 index 0000000000..0096fdb097 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/listeners/workflow/shipment.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/mailer.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/mailer.xml new file mode 100644 index 0000000000..8572779c4d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/mailer.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/message_handlers.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/message_handlers.xml index c0511cb581..7a7afcb088 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/message_handlers.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/message_handlers.xml @@ -23,7 +23,13 @@ + + + + + + @@ -35,9 +41,16 @@ + + + + + + + - + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history.xml new file mode 100644 index 0000000000..47f12b997f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/checkers.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/checkers.xml new file mode 100644 index 0000000000..2a3d76f382 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/checkers.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/command_dispatcher.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/command_dispatcher.xml new file mode 100644 index 0000000000..85158ba689 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/command_dispatcher.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + %sylius_core.price_history.batch_size% + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/command_handler.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/command_handler.xml new file mode 100644 index 0000000000..eb42a61494 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/command_handler.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/listeners.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/listeners.xml new file mode 100644 index 0000000000..abd6cebc9b --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/listeners.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/logger.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/logger.xml new file mode 100644 index 0000000000..2e1c9c0c89 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/logger.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/processors.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/processors.xml new file mode 100644 index 0000000000..b4c399eb57 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/price_history/processors.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/product_variant_map.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/product_variant_map.xml new file mode 100644 index 0000000000..9f7e3ffdf3 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/product_variant_map.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/promotion.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/promotion.xml index f66b49d52b..369f51090c 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/promotion.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/promotion.xml @@ -50,12 +50,15 @@ - - - - + + + The "%service_id%" service is deprecated since 1.13 and will be removed in 2.0. Use "%alias_id%" instead + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/providers.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/providers.xml index ef51a0c40a..dc961ebd9e 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/providers.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/providers.xml @@ -32,8 +32,38 @@ - + %locale% + + + + + + + + + + + + + + %sylius_core.orders_statistics.intervals_map% + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/state_resolvers.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/state_resolvers.xml index 639b582a24..ea05a773da 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/state_resolvers.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/state_resolvers.xml @@ -16,18 +16,18 @@ - + - + - + - + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/taxation.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/taxation.xml index f40c03c816..d3da33b1d5 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/taxation.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/taxation.xml @@ -27,6 +27,8 @@ + + @@ -34,29 +36,25 @@ + + order_items_based - - - - + order_item_units_based - - - - + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/templating.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/templating.xml index 357e3c234b..a2b97176f8 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/templating.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/templating.xml @@ -36,7 +36,7 @@ - + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/services/validators.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/services/validators.xml index d1e52a5613..b0b0639c83 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/services/validators.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/services/validators.xml @@ -17,6 +17,7 @@ + @@ -37,6 +38,10 @@ + + + + @@ -62,5 +67,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %sylius_order.resend_order_confirmation_email.order_states% + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/test_services.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/test_services.xml index d100501cda..a9942c18e8 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/test_services.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/test_services.xml @@ -45,10 +45,6 @@ %locale% - - - - diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/CatalogPromotionAction.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/CatalogPromotionAction.xml new file mode 100644 index 0000000000..70f737ea18 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/CatalogPromotionAction.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/CatalogPromotionScope.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/CatalogPromotionScope.xml index c3087942dc..41e45185d7 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/CatalogPromotionScope.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/CatalogPromotionScope.xml @@ -13,8 +13,87 @@ - - - + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/Channel.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/Channel.xml index 39a356f4a4..5f42968950 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/Channel.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/Channel.xml @@ -54,5 +54,8 @@
+ + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ChannelPriceHistoryConfig.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ChannelPriceHistoryConfig.xml new file mode 100644 index 0000000000..09aec35020 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ChannelPriceHistoryConfig.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ChannelPricing.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ChannelPricing.xml index 0888a1a499..210af52aba 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ChannelPricing.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ChannelPricing.xml @@ -13,6 +13,14 @@ + + + + + @@ -34,5 +42,14 @@ + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/OrderItem.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/OrderItem.xml index 15489179c5..694e0734b9 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/OrderItem.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/OrderItem.xml @@ -13,11 +13,18 @@ + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/PaymentMethod.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/PaymentMethod.xml index 43488cbe7d..f8644a3153 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/PaymentMethod.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/PaymentMethod.xml @@ -14,6 +14,9 @@ + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/Product.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/Product.xml index 3f5a20ab5b..395f244eba 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/Product.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/Product.xml @@ -13,11 +13,14 @@ - - - + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductImage.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductImage.xml index c3d95e9529..18be25c30d 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductImage.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductImage.xml @@ -13,6 +13,10 @@ + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductReview.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductReview.xml index 5d3c2b53e6..bc424e1019 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductReview.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductReview.xml @@ -16,5 +16,13 @@ + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductTaxon.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductTaxon.xml new file mode 100644 index 0000000000..2fff2e749d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductTaxon.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductVariant.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductVariant.xml index 54bf58747a..a2dcc55615 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductVariant.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ProductVariant.xml @@ -60,8 +60,14 @@ + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/PromotionAction.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/PromotionAction.xml new file mode 100644 index 0000000000..669ed521d4 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/PromotionAction.xml @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/PromotionRule.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/PromotionRule.xml new file mode 100644 index 0000000000..b5ccc3aef5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/PromotionRule.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ResendOrderConfirmationEmail.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ResendOrderConfirmationEmail.xml new file mode 100644 index 0000000000..34052a4ac2 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ResendOrderConfirmationEmail.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ResendShipmentConfirmationEmail.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ResendShipmentConfirmationEmail.xml new file mode 100644 index 0000000000..8c6ec356c5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ResendShipmentConfirmationEmail.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ShippingMethod.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ShippingMethod.xml index a68862a8f7..26a8dd67df 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ShippingMethod.xml +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ShippingMethod.xml @@ -19,5 +19,33 @@
+ + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ShippingMethodRule.xml b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ShippingMethodRule.xml new file mode 100644 index 0000000000..26acb1e322 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Resources/config/validation/ShippingMethodRule.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml index 38a392def7..909fec71e4 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml +++ b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.en.yml @@ -35,12 +35,20 @@ sylius: subject: 'User registration' welcome_to_our_store: 'Welcome to our store!' you_have_just_been_registered: 'You have just been registered. Thank you' + #deprecated since Sylius 1.13 and will be removed in 2.0, use user.verification instead verification_token: hello: 'Hello' message: 'Verify your account with token: ' subject: 'Email address verification' to_verify_your_email_address: 'To verify your email address - click the link below' verify_your_email_address: 'Verify your email address' + user: + account_verification: + greeting: 'Hello' + message: 'Verify your account with token: ' + statement: 'Verify your email address' + strategy: 'To verify your email address - click the link below' + subject: 'Email address verification' form: block: body: Body diff --git a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.fr.yml b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.fr.yml index 9b577bfe45..13d64c0b37 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.fr.yml +++ b/src/Sylius/Bundle/CoreBundle/Resources/translations/messages.fr.yml @@ -38,12 +38,20 @@ sylius: subject: 'Inscription utilisateur' welcome_to_our_store: 'Bienvenue dans notre boutique !' you_have_just_been_registered: 'Vous venez d''être enregistré(e). Merci' + #deprecated since Sylius 1.13 and will be removed in 2.0, use user.verification instead verification_token: hello: 'Bonjour' message: 'Vérifiez votre compte avec le jeton : ' subject: 'Vérification de l''adresse e-mail' to_verify_your_email_address: 'Pour vérifier votre adresse e-mail - cliquez sur le lien ci-dessous' verify_your_email_address: 'Vérifiez votre adresse e-mail' + user: + account_verification: + greeting: 'Bonjour' + message: 'Vérifiez votre compte avec le jeton : ' + statement: 'Vérifiez votre adresse e-mail' + strategy: 'Pour vérifier votre adresse e-mail - cliquez sur le lien ci-dessous' + subject: 'Vérification de l''adresse e-mail' form: block: body: Corps diff --git a/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.en.yml index eb7306a55a..ee3ea305f6 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.en.yml @@ -26,12 +26,15 @@ sylius: for_products: invalid_products: Provided configuration contains errors. Please add only existing products. not_empty: Provided configuration contains errors. Please add at least 1 product. + unique: Provided configuration contains errors. Please add only unique products. for_variants: invalid_variants: Provided configuration contains errors. Please add only existing variants. not_empty: Please add at least 1 variant. + unique: Provided configuration contains errors. Please add only unique variants. for_taxons: invalid_taxons: Provided configuration contains errors. Please add only existing taxons. not_empty: Provided configuration contains errors. Please add at least 1 taxon. + unique: Provided configuration contains errors. Please add only unique taxons. channel: base_currency: not_blank: Please enter channel base currency. @@ -42,7 +45,14 @@ sylius: invalid: This email is invalid. max: Email must not be longer than {{ limit }} characters. min: Email must be at least {{ limit }} characters long. + channel_price_history_config: + lowest_price_for_discounted_products_checking_period: + greater_than: 'Value must be greater than {{ compared_value }}' + less_than: 'Value must be less than {{ compared_value }}' channel_pricing: + channel_code: + not_blank: Please set channel code. + unique: This channel already has a price for this product variant. price: min: Price cannot be lower than 0. not_blank: Please enter the price. @@ -59,14 +69,21 @@ sylius: not_blank: Please enter your email. message: not_blank: Please enter your message. + country: + code: + not_exist: Country with code {{ code }} does not exist. currency: enabled: cannot_disable_base: The base currency cannot be disabled. customer: currency_code: not_valid: The currency code you entered is invalid. + customer_group: + code: + not_exist: Customer group with code {{ code }} does not exist. cart_item: not_available: '%itemName% does not have sufficient stock.' + insufficient_stock: 'Insufficient stock' order: currency_code: not_valid: The currency code you entered is invalid. @@ -75,19 +92,35 @@ sylius: product_eligibility: 'This product %productName% has been disabled.' shipping_method_eligibility: 'Product does not fit requirements for %shippingMethodName% shipping method. Please reselect your shipping method.' shipping_method_not_available: 'The "%shippingMethodName%" shipping method is not available. Please reselect your shipping method.' + resend_order_confirmation_email: + invalid_order_state: 'Cannot resend order confirmation email for order with state %state%.' + resend_shipment_confirmation_email: + invalid_shipment_state: 'Cannot resend shipment confirmation email for shipment in state %state%.' locale: enabled: cannot_disable_base: The base locale cannot be disabled. product: variants: all_prices_defined: You have to define product variants' prices for newly assigned channels first. + code: + not_exist: Product with code {{ code }} does not exist. product_attribute: invalid: Position must be an integer. product_image: file: max_size: The image is too big - {{ size }}{{ suffix }}. Maximum allowed size is {{ limit }}{{ suffix }}. upload_ini_size: The image is too big. Maximum allowed size is {{ limit }}{{ suffix }}. + product_variant: + not_belong_to_owner: 'The product variant with code "%productVariantCode%" does not belong to the product with code "%ownerCode%", which is the owner of the image.' + product_taxon: + unique: Product taxons cannot be duplicated. + product: + not_blank: Please select a product. + taxon: + not_blank: Please select a taxon. product_variant: + code: + not_exist: Product variant with code {{ code }} does not exist. onHand: min: On hand must be greater than {{ limit }}. not_blank: Please enter on hand. @@ -107,7 +140,8 @@ sylius: min: Depth cannot be negative. invalid: Depth must be a number. channel_pricing: - all_defined: 'You must define price for every channel.' + all_defined: 'You must define price for every enabled channel.' + existing_code: Channel with code {{ channelCode }} does not exist. promotion_coupon: per_customer_usage_limit: min: Coupon usage limit per customer must be at least {{ limit }}. @@ -117,12 +151,21 @@ sylius: review: author: not_blank: Please enter your email. + product: + not_blank: Please enter a product. rating: range: Review rating must be an integer in the range 1-5. + not_in_range: Review rating must be between {{ min }} and {{ max }}. + taxon: + code: + not_exist: Taxon with code {{ code }} does not exist. taxon_image: file: max_size: The image is too big - {{ size }}{{ suffix }}. Maximum allowed size is {{ limit }}{{ suffix }}. upload_ini_size: The image is too big. Maximum allowed size is {{ limit }}{{ suffix }}. + translation: + locale_code: + invalid: 'Please choose one of the available locales: %locales%' user: email: unique: This email is already used. diff --git a/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.fr.yml b/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.fr.yml index 44606da58d..5309d5bb1d 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.fr.yml +++ b/src/Sylius/Bundle/CoreBundle/Resources/translations/validators.fr.yml @@ -29,12 +29,15 @@ sylius: for_products: invalid_products: La configuration fournie contient des erreurs. Veuillez ajouter uniquement des produits existants. not_empty: La configuration fournie contient des erreurs. Veuillez ajouter au moins 1 produit. + unique: La configuration fournie contient des erreurs. Veuillez ajouter uniquement des produits uniques. for_variants: invalid_variants: La configuration fournie contient des erreurs. Veuillez ajouter uniquement des variantes existantes. not_empty: Veuillez ajouter au moins une variante. + unique: La configuration fournie contient des erreurs. Veuillez ajouter uniquement des variantes uniques. for_taxons: invalid_taxons: La configuration fournie contient des erreurs. Veuillez ajouter uniquement des taxonomies existantes. not_empty: La configuration fournie contient des erreurs. Veuillez ajouter au moins 1 taxonomie. + unique: La configuration fournie contient des erreurs. Veuillez ajouter uniquement des taxonomies uniques. channel: base_currency: not_blank: Saisissez la devise de référence du canal. @@ -45,7 +48,14 @@ sylius: invalid: Cet e-mail n'est pas valide. max: L'e-mail ne doit pas contenir plus de {{ limit }} caractères. min: L'e-mail doit contenir au moins {{ limit }} caractères. + channel_price_history_config: + lowest_price_for_discounted_products_checking_period: + greater_than: 'La valeur doit être supérieure à {{ compared_value }}' + less_than: 'La valeur doit être inférieure à {{ compared_value }}' channel_pricing: + channel_code: + not_blank: Veuillez définir le code du canal. + unique: Ce canal a déjà un prix pour cette variante de produit. price: min: Le prix ne peut pas être inférieur à 0. not_blank: Veuillez saisir un prix. @@ -62,14 +72,21 @@ sylius: not_blank: Veuillez saisir votre adresse email. message: not_blank: Veuillez saisir votre message. + country: + code: + not_exist: Le pays avec le code {{ code }} n'existe pas. currency: enabled: cannot_disable_base: La devise de base ne peut pas être désactivée. customer: currency_code: not_valid: Le code de devise que vous avez entré n’est pas valide. + customer_group: + code: + not_exist: Le groupe de clients avec le code {{ code }} n'existe pas. cart_item: not_available: '%itemName% est en rupture de stock.' + insufficient_stock: 'Stock insuffisant' order: currency_code: not_valid: Le code de devise que vous avez entré n’est pas valide. @@ -78,19 +95,35 @@ sylius: product_eligibility: 'Le produit %productName% a été désactivé.' shipping_method_eligibility: 'Ce produit n''est pas éligible au mode de livraison %shippingMethodName%. Veuillez sélectionner une nouvelle fois votre mode de livraison.' shipping_method_not_available: 'Le mode d''expédition "%shippingMethodName%" n''est pas disponible. Veuillez sélectionner à nouveau votre mode de livraison.' + resend_order_confirmation_email: + invalid_order_state: 'Impossible de renvoyer l''e-mail de confirmation de commande pour la commande avec le statut %state%.' + resend_shipment_confirmation_email: + invalid_shipment_state: 'Impossible de renvoyer l''e-mail de confirmation d''expédition pour l''expédition dans l''état %state%.' locale: enabled: cannot_disable_base: La base locale ne peut pas être désactivée. product: variants: all_prices_defined: Vous devez d’abord définir le prix des variantes de produit pour les canaux nouvellement affectés. + code: + not_exist: Le produit avec le code {{ code }} n'existe pas. product_attribute: invalid: La position doit être un entier positif. product_image: file: max_size: L'image est trop grande - {{ size }}{{ suffix }}. La taille maximum autorisée est de {{ limit }}{{ suffix }}. upload_ini_size: L'image est trop grande. La taille maximum autorisée est de {{ limit }}{{ suffix }}. + product_variant: + not_belong_to_owner: 'La variante de produit avec le code "%productVariantCode%" n''appartient pas au produit avec le code "%ownerCode%", qui est le propriétaire de l''image.' + product_taxon: + unique: Les taxons de produits ne peuvent pas être dupliqués. + product: + not_blank: Veuillez sélectionner un produit. + taxon: + not_blank: Veuillez sélectionner un taxon. product_variant: + code: + not_exist: La variante de produit avec le code {{ code }} n'existe pas. onHand: min: Le stock disponible doit être plus grand que {{ limit }}. not_blank: Veuillez saisir le stock disponible. @@ -110,7 +143,8 @@ sylius: min: La profondeur ne peut pas être négative. invalid: La profondeur doit être un nombre. channel_pricing: - all_defined: 'Vous devez définir un prix pour tous les canaux.' + all_defined: 'Vous devez définir le prix pour chaque canal activé.' + existing_code: Le canal avec le code {{ channelCode }} n'existe pas. promotion_coupon: per_customer_usage_limit: min: La limite d'utilisation du coupon par client doit être d'au moins {{ limit }}. @@ -120,12 +154,21 @@ sylius: review: author: not_blank: Veuillez saisir votre adresse email. + product: + not_blank: Veuillez saisir un produit. rating: range: La note de l'avis doit être un entier entre 1 et 5. + not_in_range: L'évaluation doit être comprise entre {{ min }} et {{ max }}. + taxon: + code: + not_exist: La taxonomie avec le code {{ code }} n'existe pas. taxon_image: file: max_size: L'image est trop grande - {{ size }}{{ suffix }}. La taille maximum autorisée est de {{ limit }}{{ suffix }}. upload_ini_size: L'image est trop grande. La taille maximum autorisée est de {{ limit }}{{ suffix }}. + translation: + locale_code: + invalid: 'Veuillez choisir l''une des langues disponibles : %locales%' user: email: unique: Cet e-mail est déjà utilisé. diff --git a/src/Sylius/Bundle/CoreBundle/Resources/views/Email/accountVerification.html.twig b/src/Sylius/Bundle/CoreBundle/Resources/views/Email/accountVerification.html.twig index f725f086a1..c92e071e3c 100644 --- a/src/Sylius/Bundle/CoreBundle/Resources/views/Email/accountVerification.html.twig +++ b/src/Sylius/Bundle/CoreBundle/Resources/views/Email/accountVerification.html.twig @@ -1,27 +1,11 @@ {% extends '@SyliusCore/Email/layout.html.twig' %} {% block subject %} - {{ 'sylius.email.verification_token.subject'|trans({}, null, localeCode)|raw }} + {{ 'sylius.email.user.account_verification.subject'|trans({}, null, localeCode)|raw }} {% endblock %} {% block content %} - {% if sylius_bundle_loaded_checker('SyliusShopBundle') %} - {% set url = sylius_channel_url(path('sylius_shop_user_verification', { 'token': user.emailVerificationToken, '_locale': localeCode }), channel) %} -
-
- {{ 'sylius.email.verification_token.hello'|trans({}, null, localeCode) }} {{ user.username }}
-
- {{ 'sylius.email.verification_token.to_verify_your_email_address'|trans({}, null, localeCode) }}: -
- - - {% else %}
- {{ 'sylius.email.verification_token.message'|trans({}, null, localeCode) }}{{ user.emailVerificationToken }} + {{ 'sylius.email.user.account_verification.message'|trans({}, null, localeCode) }}{{ user.emailVerificationToken }}
- {% endif %} {% endblock %} diff --git a/src/Sylius/Bundle/CoreBundle/Security/UserImpersonator.php b/src/Sylius/Bundle/CoreBundle/Security/UserImpersonator.php index 7c458c003b..dc61fe7a4e 100644 --- a/src/Sylius/Bundle/CoreBundle/Security/UserImpersonator.php +++ b/src/Sylius/Bundle/CoreBundle/Security/UserImpersonator.php @@ -39,7 +39,14 @@ final class UserImpersonator implements UserImpersonatorInterface $this->firewallContextName = $firewallContextName; if ($requestStackOrSession instanceof SessionInterface) { - trigger_deprecation('sylius/core-bundle', '1.12', sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', SessionInterface::class, self::class, RequestStack::class)); + trigger_deprecation( + 'sylius/core-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + SessionInterface::class, + self::class, + RequestStack::class, + ); } } diff --git a/src/Sylius/Bundle/CoreBundle/Security/UserPasswordResetter.php b/src/Sylius/Bundle/CoreBundle/Security/UserPasswordResetter.php index 9696766f03..bf1cc63db4 100644 --- a/src/Sylius/Bundle/CoreBundle/Security/UserPasswordResetter.php +++ b/src/Sylius/Bundle/CoreBundle/Security/UserPasswordResetter.php @@ -13,10 +13,10 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Security; +use Sylius\Bundle\UserBundle\Exception\UserNotFoundException; use Sylius\Component\User\Model\UserInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; use Sylius\Component\User\Security\PasswordUpdaterInterface; -use Webmozart\Assert\Assert; final class UserPasswordResetter implements UserPasswordResetterInterface { @@ -27,12 +27,17 @@ final class UserPasswordResetter implements UserPasswordResetterInterface ) { } + /** + * @throws UserNotFoundException + */ public function reset(string $token, string $password): void { /** @var UserInterface|null $user */ $user = $this->userRepository->findOneBy(['passwordResetToken' => $token]); - Assert::notNull($user, 'No user found with reset token: ' . $token); + if (null === $user) { + throw new UserNotFoundException(message: sprintf('No user found with reset token: %s', $token)); + } $lifetime = new \DateInterval($this->tokenTtl); diff --git a/src/Sylius/Bundle/CoreBundle/Storage/CartSessionStorage.php b/src/Sylius/Bundle/CoreBundle/Storage/CartSessionStorage.php index 1b5fada57d..55308d085d 100644 --- a/src/Sylius/Bundle/CoreBundle/Storage/CartSessionStorage.php +++ b/src/Sylius/Bundle/CoreBundle/Storage/CartSessionStorage.php @@ -30,7 +30,15 @@ final class CartSessionStorage implements CartStorageInterface private OrderRepositoryInterface $orderRepository, ) { if ($requestStackOrSession instanceof SessionInterface) { - trigger_deprecation('sylius/core-bundle', '1.12', sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', SessionInterface::class, self::class, RequestStack::class)); + trigger_deprecation( + 'sylius/core-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + SessionInterface::class, + self::class, + RequestStack::class, + ) + ; } } diff --git a/src/Sylius/Bundle/CoreBundle/SyliusCoreBundle.php b/src/Sylius/Bundle/CoreBundle/SyliusCoreBundle.php index b84f19d210..577bdf36f9 100644 --- a/src/Sylius/Bundle/CoreBundle/SyliusCoreBundle.php +++ b/src/Sylius/Bundle/CoreBundle/SyliusCoreBundle.php @@ -25,12 +25,14 @@ use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\BackwardsCompatibility use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\BackwardsCompatibility\ResolveShopUserTargetEntityPass; use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\BackwardsCompatibility\Symfony5AuthenticationManagerPass; use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\BackwardsCompatibility\Symfony6PrivateServicesPass; +use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\CheckStatisticsOrdersTotalsProviderTypePass; use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\CircularDependencyBreakingErrorListenerPass; use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\IgnoreAnnotationsPass; use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\LazyCacheWarmupPass; use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\LiipImageFiltersPass; use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\RegisterTaxCalculationStrategiesPass; use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\RegisterUriBasedSectionResolverPass; +use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\SyliusPriceHistoryLegacyAliasesPass; use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\TranslatableEntityLocalePass; use Sylius\Bundle\CoreBundle\Doctrine\ORM\SqlWalker\OrderByIdentifierSqlWalker; use Sylius\Bundle\ResourceBundle\AbstractResourceBundle; @@ -40,15 +42,15 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; final class SyliusCoreBundle extends AbstractResourceBundle { - public const VERSION = '1.12.14-DEV'; + public const VERSION = '1.13.0-DEV'; - public const VERSION_ID = '11214'; + public const VERSION_ID = '11300'; public const MAJOR_VERSION = '1'; - public const MINOR_VERSION = '12'; + public const MINOR_VERSION = '13'; - public const RELEASE_VERSION = '14'; + public const RELEASE_VERSION = '0'; public const EXTRA_VERSION = 'DEV'; @@ -93,6 +95,8 @@ final class SyliusCoreBundle extends AbstractResourceBundle $container->addCompilerPass(new Symfony6PrivateServicesPass()); $container->addCompilerPass(new TranslatableEntityLocalePass()); $container->addCompilerPass(new CancelOrderStateMachineCallbackPass()); + $container->addCompilerPass(new SyliusPriceHistoryLegacyAliasesPass()); + $container->addCompilerPass(new CheckStatisticsOrdersTotalsProviderTypePass()); } protected function getModelNamespace(): string diff --git a/src/Sylius/Bundle/CoreBundle/Taxation/Strategy/TaxCalculationStrategy.php b/src/Sylius/Bundle/CoreBundle/Taxation/Strategy/TaxCalculationStrategy.php index f23f3ec520..2030c59c76 100644 --- a/src/Sylius/Bundle/CoreBundle/Taxation/Strategy/TaxCalculationStrategy.php +++ b/src/Sylius/Bundle/CoreBundle/Taxation/Strategy/TaxCalculationStrategy.php @@ -22,15 +22,12 @@ use Webmozart\Assert\Assert; final class TaxCalculationStrategy implements TaxCalculationStrategyInterface { - /** @var array|OrderTaxesApplicatorInterface[] */ - private array $applicators; + /** @var iterable|OrderTaxesApplicatorInterface[] */ + private iterable $applicators; - /** - * @param array|OrderTaxesApplicatorInterface[] $applicators - */ - public function __construct(private string $type, array $applicators) + public function __construct(private string $type, iterable $applicators) { - $this->assertApplicatorsHaveCorrectType($applicators); + Assert::allIsInstanceOf($applicators, OrderTaxesApplicatorInterface::class); $this->applicators = $applicators; } @@ -58,18 +55,4 @@ final class TaxCalculationStrategy implements TaxCalculationStrategyInterface { return $this->type; } - - /** - * @param array|OrderTaxesApplicatorInterface[] $applicators - * - * @throws \InvalidArgumentException - */ - private function assertApplicatorsHaveCorrectType(array $applicators): void - { - Assert::allIsInstanceOf( - $applicators, - OrderTaxesApplicatorInterface::class, - 'Order taxes applicator should have type "%2$s". Got: %s', - ); - } } diff --git a/src/Sylius/Bundle/CoreBundle/Templating/Helper/PriceHelper.php b/src/Sylius/Bundle/CoreBundle/Templating/Helper/PriceHelper.php index 079fde9f80..3c6ed77619 100644 --- a/src/Sylius/Bundle/CoreBundle/Templating/Helper/PriceHelper.php +++ b/src/Sylius/Bundle/CoreBundle/Templating/Helper/PriceHelper.php @@ -52,6 +52,24 @@ class PriceHelper extends Helper ; } + public function getLowestPriceBeforeDiscount(ProductVariantInterface $productVariant, array $context): ?int + { + Assert::keyExists($context, 'channel'); + Assert::isInstanceOf($this->productVariantPriceCalculator, ProductVariantPricesCalculatorInterface::class); + + return $this + ->productVariantPriceCalculator + ->calculateLowestPriceBeforeDiscount($productVariant, $context) + ; + } + + public function hasLowestPriceBeforeDiscount(ProductVariantInterface $productVariant, array $context): bool + { + Assert::keyExists($context, 'channel'); + + return null !== $this->getLowestPriceBeforeDiscount($productVariant, $context); + } + /** * @throws \InvalidArgumentException */ diff --git a/src/Sylius/Bundle/CoreBundle/Templating/Helper/ProductVariantsPricesHelper.php b/src/Sylius/Bundle/CoreBundle/Templating/Helper/ProductVariantsPricesHelper.php index c79529aed4..09331e9ecc 100644 --- a/src/Sylius/Bundle/CoreBundle/Templating/Helper/ProductVariantsPricesHelper.php +++ b/src/Sylius/Bundle/CoreBundle/Templating/Helper/ProductVariantsPricesHelper.php @@ -15,13 +15,22 @@ namespace Sylius\Bundle\CoreBundle\Templating\Helper; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Provider\ProductVariantMap\ProductVariantsMapProvider; use Sylius\Component\Core\Provider\ProductVariantsPricesProviderInterface; use Symfony\Component\Templating\Helper\Helper; +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see ProductVariantsMapProvider} instead. */ class ProductVariantsPricesHelper extends Helper { public function __construct(private ProductVariantsPricesProviderInterface $productVariantsPricesProvider) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + self::class, + ProductVariantsMapProvider::class, + ); } public function getPrices(ProductInterface $product, ChannelInterface $channel): array diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Console/Command/ClearPriceHistoryCommandTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Console/Command/ClearPriceHistoryCommandTest.php new file mode 100644 index 0000000000..cf6aaa56d0 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Console/Command/ClearPriceHistoryCommandTest.php @@ -0,0 +1,124 @@ +remover = $this->createMock(ChannelPricingLogEntriesRemoverInterface::class); + + $this->commandTester = new CommandTester(new ClearPriceHistoryCommand($this->remover)); + } + + /** + * @test + * + * @dataProvider getInvalidDays + */ + public function it_does_not_clear_pricing_history_when_number_of_days_is_invalid(mixed $days): void + { + $this->remover->expects($this->never())->method('remove'); + + $this->commandTester->execute(['days' => $days]); + + $this->assertSame(Command::FAILURE, $this->commandTester->getStatusCode()); + $this->assertStringContainsString( + 'Number of days must be an integer greater than 0', + $this->commandTester->getDisplay(), + ); + } + + /** + * @test + * + * @dataProvider getValidDays + */ + public function it_clears_pricing_history_when_non_interactive(int|string $days): void + { + $this->remover->expects($this->once())->method('remove'); + + $this->commandTester->execute(['days' => $days], ['interactive' => false]); + + $this->commandTester->assertCommandIsSuccessful(); + } + + /** @test */ + public function it_asks_for_confirmation_when_interactive(): void + { + $this->remover->expects($this->once())->method('remove'); + + $this->commandTester->setInputs(['yes']); + $this->commandTester->execute(['days' => 30], ['interactive' => true]); + + $this->assertStringContainsString( + 'Are you sure you want to clear the price history from before 30 days ago?', + $this->commandTester->getDisplay(), + ); + $this->assertSame(Command::SUCCESS, $this->commandTester->getStatusCode()); + } + + /** @test */ + public function it_does_nothing_when_user_does_not_confirm(): void + { + $this->remover->expects($this->never())->method('remove'); + + $this->commandTester->setInputs(['no']); + $this->commandTester->execute(['days' => 30], ['interactive' => true]); + + $this->assertStringContainsString( + 'Are you sure you want to clear the price history from before 30 days ago?', + $this->commandTester->getDisplay(), + ); + $this->assertSame(Command::INVALID, $this->commandTester->getStatusCode()); + } + + public function getInvalidDays(): iterable + { + yield [0]; + yield ['0']; + yield [0.0]; + yield ['0.0']; + yield [-1]; + yield ['-1']; + yield [-1.0]; + yield ['-1.0']; + yield [1.1]; + yield ['1.1']; + yield ['a']; + } + + public function getValidDays(): iterable + { + yield [1]; + yield ['1']; + yield [30]; + yield ['30']; + yield [60]; + yield ['60']; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/Compiler/BackwardsCompatibility/CancelOrderStateMachineCallbackPassTest.php b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/Compiler/BackwardsCompatibility/CancelOrderStateMachineCallbackPassTest.php index c79934b33f..af175c6e6d 100644 --- a/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/Compiler/BackwardsCompatibility/CancelOrderStateMachineCallbackPassTest.php +++ b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/Compiler/BackwardsCompatibility/CancelOrderStateMachineCallbackPassTest.php @@ -17,7 +17,7 @@ use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase use Sylius\Bundle\CoreBundle\DependencyInjection\Compiler\BackwardsCompatibility\CancelOrderStateMachineCallbackPass; use Symfony\Component\DependencyInjection\ContainerBuilder; -class CancelOrderStateMachineCallbackPassTest extends AbstractCompilerPassTestCase +final class CancelOrderStateMachineCallbackPassTest extends AbstractCompilerPassTestCase { public array $smConfigs = [ 'sylius_order' => [ @@ -74,26 +74,10 @@ class CancelOrderStateMachineCallbackPassTest extends AbstractCompilerPassTestCa ], ]; - /** @test */ - public function it_triggers_deprecation_error_when_old_callback_name_is_used(): void - { - $this->setParameter('sm.configs', $this->smConfigs); - - $this->expectDeprecation(); - $this->expectDeprecationMessage(sprintf( - 'Callback "%s" was renamed to "%s". The old name will be removed in Sylius 2.0, use the new name to override it.', - 'winzou_state_machine.sylius_order.callbacks.after.sylis_cancel_order', - 'winzou_state_machine.sylius_order.callbacks.after.sylius_cancel_order', - )); - - $this->compile(); - } - /** @test */ public function it_converts_from_old_name_to_new_name(): void { $this->setParameter('sm.configs', $this->smConfigs); - $this->expectDeprecation(); $this->compile(); $smConfigs = $this->container->getParameter('sm.configs'); diff --git a/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/Compiler/CheckStatisticsOrdersTotalsProviderTypePassTest.php b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/Compiler/CheckStatisticsOrdersTotalsProviderTypePassTest.php new file mode 100644 index 0000000000..80547b9676 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/Compiler/CheckStatisticsOrdersTotalsProviderTypePassTest.php @@ -0,0 +1,80 @@ +setParameter('sylius_core.orders_statistics.intervals_map', ['daily' => 'Daily', 'monthly' => 'Monthly']); + $this->setDefinition( + 'sylius.registry.statistics_orders_totals_provider', + (new Definition()) + ->addTag('sylius.statistics.orders_totals_provider', ['type' => 'daily']) + ->addTag('sylius.statistics.orders_totals_provider', ['type' => 'monthly']), + ); + + $this->compile(); + + $this->assertContainerBuilderHasService('sylius.registry.statistics_orders_totals_provider'); + $this->assertContainerBuilderHasParameter( + 'sylius_core.orders_statistics.intervals_map', + ['daily' => 'Daily', 'monthly' => 'Monthly'], + ); + } + + /** @test */ + public function it_throws_exception_if_statistics_orders_totals_provider_type_is_not_defined(): void + { + $this->setParameter('sylius_core.orders_statistics.intervals_map', ['daily' => 'Daily', 'monthly' => 'Monthly']); + $this->setDefinition( + 'sylius.registry.statistics_orders_totals_provider', + (new Definition()) + ->addTag('sylius.statistics.orders_totals_provider', ['type' => 'daily']) + ->addTag('sylius.statistics.orders_totals_provider', []), + ); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Tagged orders totals providers need to have `type` attribute.'); + + $this->compile(); + } + + /** @test */ + public function it_throws_exception_if_statistics_orders_totals_provider_type_is_incorrect(): void + { + $this->setParameter('sylius_core.orders_statistics.intervals_map', ['daily' => 'Daily', 'monthly' => 'Monthly']); + $this->setDefinition( + 'sylius.registry.statistics_orders_totals_provider', + (new Definition()) + ->addTag('sylius.statistics.orders_totals_provider', ['type' => 'daily']), + ); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('There is no orders totals provider for interval type "monthly"'); + + $this->compile(); + } + + protected function registerCompilerPass(ContainerBuilder $container): void + { + $container->addCompilerPass(new CheckStatisticsOrdersTotalsProviderTypePass()); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPassTest.php b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPassTest.php new file mode 100644 index 0000000000..22e15af03d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/Compiler/SyliusPriceHistoryLegacyAliasesPassTest.php @@ -0,0 +1,47 @@ +register('Sylius\Component\Core\Checker\ProductVariantLowestPriceDisplayCheckerInterface'); + $container->register('Sylius\Bundle\CoreBundle\PriceHistory\CommandHandler\SomeService'); + $container->register('sylius.manager.channel_price_history_config'); + $container->register('sylius.factory.channel_pricing_log_entry'); + + $this->process($container); + + $this->assertHasAlias($container, 'Sylius\PriceHistoryPlugin\Application\Checker\ProductVariantLowestPriceDisplayCheckerInterface'); + $this->assertHasAlias($container, 'Sylius\PriceHistoryPlugin\Application\CommandHandler\SomeService'); + $this->assertHasAlias($container, 'sylius_price_history.manager.channel_price_history_config'); + $this->assertHasAlias($container, 'sylius_price_history.factory.channel_pricing_log_entry'); + } + + private function assertHasAlias(ContainerBuilder $container, string $alias): void + { + $this->assertTrue($container->hasAlias($alias), 'Expected to find alias ' . $alias); + } + + private function process(ContainerBuilder $container): void + { + (new SyliusPriceHistoryLegacyAliasesPass())->process($container); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/ConfigurationTest.php index f2333fba14..4a0ebfe414 100644 --- a/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -72,6 +72,118 @@ final class ConfigurationTest extends TestCase ); } + public function it_allows_to_configure_orders_statistics_intervals_map(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'orders_statistics' => [ + 'intervals_map' => [ + 'day' => [ + 'interval' => 'P1D', + 'period_format' => 'Y-m-d', + ], + 'month' => [ + 'interval' => 'P1M', + 'period_format' => 'Y-m', + ], + ], + ], + ], + ], + [ + 'orders_statistics' => [ + 'intervals_map' => [ + 'day' => [ + 'interval' => 'P1D', + 'period_format' => 'Y-m-d', + ], + 'month' => [ + 'interval' => 'P1M', + 'period_format' => 'Y-m', + ], + ], + ], + ], + 'orders_statistics', + ); + } + + /** @test */ + public function it_throws_an_exception_if_orders_statistics_intervals_map_interval_is_empty(): void + { + $this->assertConfigurationIsInvalid( + [['orders_statistics' => ['intervals_map' => ['day' => ['interval' => '', 'period_format' => 'Y-m-d']]]]], + 'The path "sylius_core.orders_statistics.intervals_map.day.interval" cannot contain an empty value, but got "".', + ); + } + + /** @test */ + public function it_throws_an_exception_if_orders_statistics_intervals_map_interval_is_invalid(): void + { + $this->assertConfigurationIsInvalid( + [['orders_statistics' => ['intervals_map' => ['day' => ['interval' => 'invalid', 'period_format' => 'Y-m-d']]]]], + 'Invalid format for interval ""invalid"". Expected a string compatible with DateInterval.', + ); + } + + /** @test */ + public function it_throws_an_exception_if_orders_statistics_intervals_map_period_format_is_empty(): void + { + $this->assertConfigurationIsInvalid( + [['orders_statistics' => ['intervals_map' => ['day' => ['interval' => 'P1D', 'period_format' => '']]]]], + 'The path "sylius_core.orders_statistics.intervals_map.day.period_format" cannot contain an empty value, but got "".', + ); + } + + /** @test */ + public function it_allows_to_configure_a_default_state_machine_adapter(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'state_machine' => [ + 'default_adapter' => 'symfony_workflow', + ], + ], + ], + [ + 'state_machine' => [ + 'default_adapter' => 'symfony_workflow', + 'graphs_to_adapters_mapping' => [], + ], + ], + 'state_machine', + ); + } + + /** @test */ + public function it_allows_to_configure_the_state_machines_adapters_mapping(): void + { + $this->assertProcessedConfigurationEquals( + [ + [ + 'state_machine' => [ + 'graphs_to_adapters_mapping' => [ + 'order' => 'symfony_workflow', + 'payment' => 'winzou_state_machine', + ], + ], + ], + ], + [ + 'state_machine' => [ + 'default_adapter' => 'winzou_state_machine', + 'graphs_to_adapters_mapping' => [ + 'order' => 'symfony_workflow', + 'payment' => 'winzou_state_machine', + ], + ], + ], + 'state_machine', + ); + } + /** @test */ public function it_throws_an_exception_if_value_other_then_integer_is_declared_as_batch_size(): void { @@ -94,6 +206,26 @@ final class ConfigurationTest extends TestCase ); } + /** @test */ + public function it_does_not_autoconfigure_with_attributes_by_default(): void + { + $this->assertProcessedConfigurationEquals( + [[]], + ['autoconfigure_with_attributes' => false], + 'autoconfigure_with_attributes', + ); + } + + /** @test */ + public function it_allows_to_enable_autoconfiguring_with_attributes(): void + { + $this->assertProcessedConfigurationEquals( + [['autoconfigure_with_attributes' => true]], + ['autoconfigure_with_attributes' => true], + 'autoconfigure_with_attributes', + ); + } + protected function getConfiguration(): ConfigurationInterface { return new Configuration(); diff --git a/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/SyliusCoreExtensionTest.php b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/SyliusCoreExtensionTest.php index 409c78e5cf..f1bb2eedb1 100644 --- a/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/SyliusCoreExtensionTest.php +++ b/src/Sylius/Bundle/CoreBundle/Tests/DependencyInjection/SyliusCoreExtensionTest.php @@ -16,11 +16,31 @@ namespace Sylius\Bundle\CoreBundle\Tests\DependencyInjection; use Doctrine\Bundle\MigrationsBundle\DependencyInjection\DoctrineMigrationsExtension; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; use Matthias\SymfonyDependencyInjectionTest\PhpUnit\DefinitionHasTagConstraint; +use Sylius\Bundle\CoreBundle\Attribute\AsCatalogPromotionApplicatorCriteria; +use Sylius\Bundle\CoreBundle\Attribute\AsCatalogPromotionPriceCalculator; +use Sylius\Bundle\CoreBundle\Attribute\AsEntityObserver; +use Sylius\Bundle\CoreBundle\Attribute\AsOrderItemsTaxesApplicator; +use Sylius\Bundle\CoreBundle\Attribute\AsOrderItemUnitsTaxesApplicator; +use Sylius\Bundle\CoreBundle\Attribute\AsOrdersTotalsProvider; +use Sylius\Bundle\CoreBundle\Attribute\AsProductVariantMapProvider; +use Sylius\Bundle\CoreBundle\Attribute\AsTaxCalculationStrategy; +use Sylius\Bundle\CoreBundle\Attribute\AsUriBasedSectionResolver; use Sylius\Bundle\CoreBundle\DependencyInjection\SyliusCoreExtension; +use Sylius\Bundle\CoreBundle\Tests\Stub\CatalogPromotionApplicatorCriteriaStub; +use Sylius\Bundle\CoreBundle\Tests\Stub\CatalogPromotionPriceCalculatorStub; +use Sylius\Bundle\CoreBundle\Tests\Stub\EntityObserverStub; +use Sylius\Bundle\CoreBundle\Tests\Stub\OrderItemsTaxesApplicatorStub; +use Sylius\Bundle\CoreBundle\Tests\Stub\OrderItemUnitsTaxesApplicatorStub; +use Sylius\Bundle\CoreBundle\Tests\Stub\OrdersTotalsProviderStub; +use Sylius\Bundle\CoreBundle\Tests\Stub\ProductVariantMapProviderStub; +use Sylius\Bundle\CoreBundle\Tests\Stub\TaxCalculationStrategyStub; +use Sylius\Bundle\CoreBundle\Tests\Stub\UriBasedSectionResolverStub; +use Sylius\Bundle\OrderBundle\DependencyInjection\SyliusOrderExtension; use Sylius\Component\Core\Filesystem\Adapter\FilesystemAdapterInterface; use Sylius\Component\Core\Filesystem\Adapter\FlysystemFilesystemAdapter; use Sylius\Component\Core\Filesystem\Adapter\GaufretteFilesystemAdapter; use SyliusLabs\DoctrineMigrationsExtraBundle\DependencyInjection\SyliusLabsDoctrineMigrationsExtraExtension; +use Symfony\Component\DependencyInjection\Definition; final class SyliusCoreExtensionTest extends AbstractExtensionTestCase { @@ -70,6 +90,34 @@ final class SyliusCoreExtensionTest extends AbstractExtensionTestCase $this->testPrependingDoctrineMigrations('dev'); } + /** + * @test + * + * @dataProvider provideAutoconfigureWithAttributesData + */ + public function it_prepends_sylius_order_bundle_configuration_with_proper_values(bool $value, bool $orderBundleValue): void + { + $this->container->setParameter('kernel.environment', 'dev'); + $this->container->registerExtension(new SyliusOrderExtension()); + $this->container->loadFromExtension('sylius_core', [ + 'autoconfigure_with_attributes' => $value, + ]); + $this->container->loadFromExtension('sylius_order', [ + 'autoconfigure_with_attributes' => $orderBundleValue, + ]); + + $this->load(); + + $syliusOrderConfig = $this->container->getExtensionConfig('sylius_order'); + $this->assertEquals($value, $syliusOrderConfig[0]['autoconfigure_with_attributes']); + } + + public static function provideAutoconfigureWithAttributesData(): iterable + { + yield [true, false]; + yield [false, true]; + } + /** @test */ public function it_does_not_autoconfigure_prepending_doctrine_migrations_if_it_is_disabled_for_test_env(): void { @@ -160,6 +208,230 @@ final class SyliusCoreExtensionTest extends AbstractExtensionTestCase $this->assertContainerBuilderHasAlias(FilesystemAdapterInterface::class, GaufretteFilesystemAdapter::class); } + /** @test */ + public function it_autoconfigures_catalog_promotion_applicator_criteria_with_attribute(): void + { + $this->container->setParameter('kernel.environment', 'prod'); + $this->container->setDefinition( + 'acme.catalog_promotion_applicator_criteria_with_attribute', + (new Definition()) + ->setClass(CatalogPromotionApplicatorCriteriaStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.catalog_promotion_applicator_criteria_with_attribute', + AsCatalogPromotionApplicatorCriteria::SERVICE_TAG, + ['priority' => 20], + ); + } + + /** @test */ + public function it_autoconfigures_catalog_promotion_price_calculator_with_attribute(): void + { + $this->container->setParameter('kernel.environment', 'prod'); + $this->container->setDefinition( + 'acme.catalog_promotion_price_calculator_with_attribute', + (new Definition()) + ->setClass(CatalogPromotionPriceCalculatorStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.catalog_promotion_price_calculator_with_attribute', + AsCatalogPromotionPriceCalculator::SERVICE_TAG, + ['priority' => 9], + ); + } + + /** @test */ + public function it_autoconfigures_entity_observer_with_attribute(): void + { + $this->container->setParameter('kernel.environment', 'prod'); + $this->container->setDefinition( + 'acme.entity_observer_with_attribute', + (new Definition()) + ->setClass(EntityObserverStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.entity_observer_with_attribute', + AsEntityObserver::SERVICE_TAG, + ['priority' => 5], + ); + } + + /** @test */ + public function it_autoconfigures_order_items_taxes_applicator_with_attribute(): void + { + $this->container->setParameter('kernel.environment', 'prod'); + $this->container->setDefinition( + 'acme.order_items_taxes_applicator_with_attribute', + (new Definition()) + ->setClass(OrderItemsTaxesApplicatorStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.order_items_taxes_applicator_with_attribute', + AsOrderItemsTaxesApplicator::SERVICE_TAG, + ['priority' => 15], + ); + } + + /** @test */ + public function it_autoconfigures_order_item_units_taxes_applicator_with_attribute(): void + { + $this->container->setParameter('kernel.environment', 'prod'); + $this->container->setDefinition( + 'acme.order_item_units_taxes_applicator_with_attribute', + (new Definition()) + ->setClass(OrderItemUnitsTaxesApplicatorStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.order_item_units_taxes_applicator_with_attribute', + AsOrderItemUnitsTaxesApplicator::SERVICE_TAG, + ['priority' => 15], + ); + } + + /** @test */ + public function it_autoconfigures_product_variant_map_provider_with_attribute(): void + { + $this->container->setParameter('kernel.environment', 'prod'); + $this->container->setDefinition( + 'acme.product_variant_map_provider_with_attribute', + (new Definition()) + ->setClass(ProductVariantMapProviderStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.product_variant_map_provider_with_attribute', + AsProductVariantMapProvider::SERVICE_TAG, + ['priority' => 4], + ); + } + + /** @test */ + public function it_autoconfigures_tax_calculation_strategy_with_attribute(): void + { + $this->container->setParameter('kernel.environment', 'prod'); + $this->container->setDefinition( + 'acme.tax_calculation_strategy_with_attribute', + (new Definition()) + ->setClass(TaxCalculationStrategyStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.tax_calculation_strategy_with_attribute', + AsTaxCalculationStrategy::SERVICE_TAG, + [ + 'type' => 'test', + 'label' => 'Test', + 'priority' => 15, + ], + ); + } + + /** @test */ + public function it_autoconfigures_uri_based_section_resolver_with_attribute(): void + { + $this->container->setParameter('kernel.environment', 'prod'); + $this->container->setDefinition( + 'acme.uri_based_section_resolver_with_attribute', + (new Definition()) + ->setClass(UriBasedSectionResolverStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.uri_based_section_resolver_with_attribute', + AsUriBasedSectionResolver::SERVICE_TAG, + ['priority' => 20], + ); + } + + /** @test */ + public function it_autoconfigures_orders_totals_provider_with_attribute(): void + { + $this->container->setParameter('kernel.environment', 'prod'); + $this->container->setDefinition( + 'acme.orders_totals_provider', + (new Definition()) + ->setClass(OrdersTotalsProviderStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.orders_totals_provider', + AsOrdersTotalsProvider::SERVICE_TAG, + ['type' => 'stub'], + ); + } + + /** @test */ + public function it_sets_the_orders_statistics_intervals_map_parameter(): void + { + $this->container->setParameter('kernel.environment', 'prod'); + $this->load([ + 'orders_statistics' => [ + 'intervals_map' => [ + 'day' => [ + 'interval' => 'P1D', + 'period_format' => 'YYYY-MM-DD', + ], + 'month' => [ + 'interval' => 'P1M', + 'period_format' => 'YYYY-MM', + ], + ], + ], + ]); + + $this->assertContainerBuilderHasParameter('sylius_core.orders_statistics.intervals_map', [ + 'day' => [ + 'interval' => 'P1D', + 'period_format' => 'YYYY-MM-DD', + ], + 'month' => [ + 'interval' => 'P1M', + 'period_format' => 'YYYY-MM', + ], + ]); + } + protected function getContainerExtensions(): array { return [new SyliusCoreExtension()]; diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Fixture/OptionsResolver/LazyOptionTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Fixture/OptionsResolver/LazyOptionTest.php index 1ec58e35eb..f06039d117 100644 --- a/src/Sylius/Bundle/CoreBundle/Tests/Fixture/OptionsResolver/LazyOptionTest.php +++ b/src/Sylius/Bundle/CoreBundle/Tests/Fixture/OptionsResolver/LazyOptionTest.php @@ -15,6 +15,7 @@ namespace Sylius\Bundle\CoreBundle\Tests\Fixture\OptionsResolver; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Sylius\Bundle\CoreBundle\Fixture\OptionsResolver\LazyOption; use Sylius\Bundle\CoreBundle\Fixture\OptionsResolver\ResourceNotFoundException; @@ -24,6 +25,8 @@ use Symfony\Component\OptionsResolver\Options; final class LazyOptionTest extends TestCase { + use ProphecyTrait; + /** * @test */ diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Form/Type/CatalogPromotion/CatalogPromotionActionTypeTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Form/Type/CatalogPromotion/CatalogPromotionActionTypeTest.php index f1f31528db..13298daa37 100644 --- a/src/Sylius/Bundle/CoreBundle/Tests/Form/Type/CatalogPromotion/CatalogPromotionActionTypeTest.php +++ b/src/Sylius/Bundle/CoreBundle/Tests/Form/Type/CatalogPromotion/CatalogPromotionActionTypeTest.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Tests\Form\Type\CatalogPromotion; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ProphecyInterface; use Sylius\Bundle\CoreBundle\Form\Type\CatalogPromotionAction\ChannelBasedFixedDiscountActionConfigurationType; @@ -32,7 +33,9 @@ use Symfony\Component\Validator\Validation; final class CatalogPromotionActionTypeTest extends TypeTestCase { - private ProphecyInterface|ChannelInterface $channel; + use ProphecyTrait; + + private ChannelInterface|ProphecyInterface $channel; private ObjectProphecy $channelRepository; diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Form/Type/Taxon/ProductTaxonAutocompleteChoiceTypeTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Form/Type/Taxon/ProductTaxonAutocompleteChoiceTypeTest.php index 86b9c931d7..701bdbbfa7 100644 --- a/src/Sylius/Bundle/CoreBundle/Tests/Form/Type/Taxon/ProductTaxonAutocompleteChoiceTypeTest.php +++ b/src/Sylius/Bundle/CoreBundle/Tests/Form/Type/Taxon/ProductTaxonAutocompleteChoiceTypeTest.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Tests\Form\Type\Taxon; use Doctrine\Common\Collections\ArrayCollection; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Sylius\Bundle\CoreBundle\Form\Type\Taxon\ProductTaxonAutocompleteChoiceType; use Sylius\Bundle\ResourceBundle\Form\Type\ResourceAutocompleteChoiceType; @@ -29,6 +30,8 @@ use Symfony\Component\Form\Test\TypeTestCase; final class ProductTaxonAutocompleteChoiceTypeTest extends TypeTestCase { + use ProphecyTrait; + private ObjectProphecy $resourceRepositoryRegistry; private ObjectProphecy $productTaxonFactory; diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/CatalogPromotionWorkflowTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/CatalogPromotionWorkflowTest.php new file mode 100644 index 0000000000..220b7207af --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/CatalogPromotionWorkflowTest.php @@ -0,0 +1,73 @@ +getStateMachine(); + $catalogPromotion = new CatalogPromotion(); + + $stateMachine->apply($catalogPromotion, 'sylius_catalog_promotion', 'process'); + + $this->assertSame('processing', $catalogPromotion->getState()); + } + + /** @test */ + public function it_applies_available_transition_for_catalog_promotion_active_status(): void + { + $stateMachine = $this->getStateMachine(); + $catalogPromotion = new CatalogPromotion(); + $catalogPromotion->setState('active'); + + $stateMachine->apply($catalogPromotion, 'sylius_catalog_promotion', 'process'); + + $this->assertSame('processing', $catalogPromotion->getState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForProcessingState + */ + public function it_applies_all_available_transition_for_catalog_promotion_processing_status( + string $transition, + string $expectedStatus, + ): void { + $stateMachine = $this->getStateMachine(); + $catalogPromotion = new CatalogPromotion(); + $catalogPromotion->setState('processing'); + + $stateMachine->apply($catalogPromotion, 'sylius_catalog_promotion', $transition); + + $this->assertSame($expectedStatus, $catalogPromotion->getState()); + } + + public function availableTransitionsForProcessingState(): iterable + { + yield ['activate', 'active']; + yield ['deactivate', 'inactive']; + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius_abstraction.state_machine.adapter.symfony_workflow'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderCheckoutWorkflowTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderCheckoutWorkflowTest.php new file mode 100644 index 0000000000..02690bad61 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderCheckoutWorkflowTest.php @@ -0,0 +1,240 @@ +createMock(PromotionRepositoryInterface::class); + $promotionRepository + ->method('findActiveNonCouponBasedByChannel') + ->willReturn([]) + ; + + $sequenceRepository = $this->createMock(RepositoryInterface::class); + $sequenceRepository + ->method('findOneBy') + ->willReturn(null) + ; + + $this->orderShippingMethodSelectionRequirementChecker = $this->createMock(OrderShippingMethodSelectionRequirementCheckerInterface::class); + $this->orderPaymentMethodSelectionRequirementChecker = $this->createMock(OrderPaymentMethodSelectionRequirementCheckerInterface::class); + + self::getContainer()->set('sylius.checker.order_shipping_method_selection_requirement', $this->orderShippingMethodSelectionRequirementChecker); + self::getContainer()->set('sylius.checker.order_payment_method_selection_requirement', $this->orderPaymentMethodSelectionRequirementChecker); + self::getContainer()->set('sylius.repository.promotion', $promotionRepository); + self::getContainer()->set('sylius.repository.order_sequence', $sequenceRepository); + } + + /** + * @test + * + * @dataProvider availableTransitionsForCartState + */ + public function it_applies_all_available_transitions_for_order_checkout_cart_state( + string $transition, + bool $isShippingMethodSelectionRequired, + bool $isPaymentMethodSelectionRequired, + string $expectedState, + ): void { + $this->setShippingMethodSelectionRequired($isShippingMethodSelectionRequired); + $this->setPaymentMethodSelectionRequired($isPaymentMethodSelectionRequired); + $stateMachine = $this->getStateMachine(); + $order = $this->createOrderWithCheckoutState(); + + $stateMachine->apply($order, 'sylius_order_checkout', $transition); + + $this->assertSame($expectedState, $order->getCheckoutState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForAddressedState + */ + public function it_applies_all_available_transitions_for_order_checkout_addressed_state(string $transition, string $expectedState): void + { + $this->setShippingMethodSelectionRequired(true); + $this->setPaymentMethodSelectionRequired(true); + $stateMachine = $this->getStateMachine(); + $order = $this->createOrderWithCheckoutState('addressed'); + + $stateMachine->apply($order, 'sylius_order_checkout', $transition); + + $this->assertSame($expectedState, $order->getCheckoutState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForShippingSelectedState + */ + public function it_applies_all_available_transitions_for_order_checkout_shipping_selected_state(string $transition, string $expectedState): void + { + $this->setShippingMethodSelectionRequired(true); + $this->setPaymentMethodSelectionRequired(true); + $stateMachine = $this->getStateMachine(); + $order = $this->createOrderWithCheckoutState('shipping_selected'); + + $stateMachine->apply($order, 'sylius_order_checkout', $transition); + + $this->assertSame($expectedState, $order->getCheckoutState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForShippingSkippedState + */ + public function it_applies_all_available_transitions_for_order_checkout_shipping_skipped_state(string $transition, string $expectedState): void + { + $this->setShippingMethodSelectionRequired(true); + $this->setPaymentMethodSelectionRequired(true); + $stateMachine = $this->getStateMachine(); + $order = $this->createOrderWithCheckoutState('shipping_skipped'); + + $stateMachine->apply($order, 'sylius_order_checkout', $transition); + + $this->assertSame($expectedState, $order->getCheckoutState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForPaymentSkippedState + */ + public function it_applies_all_available_transitions_for_order_checkout_payment_skipped_state(string $transition, string $expectedState): void + { + $this->setShippingMethodSelectionRequired(true); + $this->setPaymentMethodSelectionRequired(true); + $stateMachine = $this->getStateMachine(); + $order = $this->createOrderWithCheckoutState('payment_skipped'); + + $stateMachine->apply($order, 'sylius_order_checkout', $transition); + + $this->assertSame($expectedState, $order->getCheckoutState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForPaymentSelectedState + */ + public function it_applies_all_available_transitions_for_order_checkout_payment_selected_state(string $transition, string $expectedState): void + { + $this->setShippingMethodSelectionRequired(true); + $this->setPaymentMethodSelectionRequired(true); + $stateMachine = $this->getStateMachine(); + $order = $this->createOrderWithCheckoutState('payment_selected'); + $stateMachine->apply($order, 'sylius_order_checkout', $transition); + + $this->assertSame($expectedState, $order->getCheckoutState()); + } + + public function availableTransitionsForCartState(): iterable + { + yield ['address', false, false, 'payment_skipped']; + yield ['address', false, true, 'shipping_skipped']; + yield ['address', true, false, 'addressed']; + yield ['address', true, true, 'addressed']; + } + + public function availableTransitionsForAddressedState(): iterable + { + yield ['address', 'addressed']; + yield ['skip_shipping', 'shipping_skipped']; + yield ['select_shipping', 'shipping_selected']; + } + + public function availableTransitionsForShippingSelectedState(): iterable + { + yield ['address', 'addressed']; + yield ['select_shipping', 'shipping_selected']; + yield ['skip_payment', 'payment_skipped']; + yield ['select_payment', 'payment_selected']; + } + + public function availableTransitionsForShippingSkippedState(): iterable + { + yield ['address', 'addressed']; + yield ['skip_payment', 'payment_skipped']; + yield ['select_payment', 'payment_selected']; + } + + public function availableTransitionsForPaymentSkippedState(): iterable + { + yield ['address', 'addressed']; + yield ['select_shipping', 'shipping_selected']; + yield ['complete', 'completed']; + } + + public function availableTransitionsForPaymentSelectedState(): iterable + { + yield ['address', 'addressed']; + yield ['select_shipping', 'shipping_selected']; + yield ['select_payment', 'payment_selected']; + yield ['complete', 'completed']; + } + + private function createOrderWithCheckoutState(string $checkoutState = 'cart'): OrderInterface + { + $channel = $this->createMock(ChannelInterface::class); + $customer = $this->createMock(Customer::class); + $order = new Order(); + $order->setChannel($channel); + $order->setCustomer($customer); + $order->setCheckoutState($checkoutState); + + return $order; + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius_abstraction.state_machine.adapter.symfony_workflow'); + } + + public function setShippingMethodSelectionRequired(bool $isShippingMethodSelectionRequired): void + { + $this->orderShippingMethodSelectionRequirementChecker + ->method('isShippingMethodSelectionRequired') + ->willReturn($isShippingMethodSelectionRequired); + } + + private function setPaymentMethodSelectionRequired(bool $isPaymentMethodSelectionRequired): void + { + $this->orderPaymentMethodSelectionRequirementChecker + ->method('isPaymentMethodSelectionRequired') + ->willReturn($isPaymentMethodSelectionRequired); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderPaymentWorkflowTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderPaymentWorkflowTest.php new file mode 100644 index 0000000000..e4d8fcd711 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderPaymentWorkflowTest.php @@ -0,0 +1,182 @@ +getStateMachine(); + $order = new Order(); + + $stateMachine->apply($order, 'sylius_order_payment', 'request_payment'); + + $this->assertSame('awaiting_payment', $order->getPaymentState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForAwaitingPaymentState + */ + public function it_applies_all_available_transitions_for_order_payment_awaiting_payment_state( + string $transition, + string $expectedStatus, + ): void { + $stateMachine = $this->getStateMachine(); + $order = new Order(); + $order->setPaymentState('awaiting_payment'); + $stateMachine->apply($order, 'sylius_order_payment', $transition); + + $this->assertSame($expectedStatus, $order->getPaymentState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForPartiallyAuthorizedState + */ + public function it_applies_all_available_transitions_for_order_payment_partially_authorized_state( + string $transition, + string $expectedStatus, + ): void { + $stateMachine = $this->getStateMachine(); + $order = new Order(); + $order->setPaymentState('partially_authorized'); + $stateMachine->apply($order, 'sylius_order_payment', $transition); + + $this->assertSame($expectedStatus, $order->getPaymentState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForAuthorizedState + */ + public function it_applies_all_available_transitions_for_order_payment_authorized_state( + string $transition, + string $expectedStatus, + ): void { + $stateMachine = $this->getStateMachine(); + $order = new Order(); + $order->setPaymentState('authorized'); + $stateMachine->apply($order, 'sylius_order_payment', $transition); + + $this->assertSame($expectedStatus, $order->getPaymentState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForPartiallyPaidState + */ + public function it_applies_all_available_transitions_for_order_payment_partially_paid_state( + string $transition, + string $expectedStatus, + ): void { + $stateMachine = $this->getStateMachine(); + $order = new Order(); + $order->setPaymentState('partially_paid'); + $stateMachine->apply($order, 'sylius_order_payment', $transition); + + $this->assertSame($expectedStatus, $order->getPaymentState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForPaidState + */ + public function it_applies_all_available_transitions_for_order_payment_paid_state( + string $transition, + string $expectedStatus, + ): void { + $stateMachine = $this->getStateMachine(); + $order = new Order(); + $order->setPaymentState('paid'); + $stateMachine->apply($order, 'sylius_order_payment', $transition); + + $this->assertSame($expectedStatus, $order->getPaymentState()); + } + + /** + * @test + * + * @dataProvider availableTransitionsForPartiallyRefundedState + */ + public function it_applies_all_available_transitions_for_order_partially_refunded_state( + string $transition, + string $expectedStatus, + ): void { + $stateMachine = $this->getStateMachine(); + $order = new Order(); + $order->setPaymentState('partially_refunded'); + $stateMachine->apply($order, 'sylius_order_payment', $transition); + + $this->assertSame($expectedStatus, $order->getPaymentState()); + } + + public function availableTransitionsForAwaitingPaymentState(): iterable + { + yield ['partially_authorize', 'partially_authorized']; + yield ['authorize', 'authorized']; + yield ['partially_pay', 'partially_paid']; + yield ['cancel', 'cancelled']; + yield ['pay', 'paid']; + } + + public function availableTransitionsForPartiallyAuthorizedState(): iterable + { + yield ['partially_authorize', 'partially_authorized']; + yield ['authorize', 'authorized']; + yield ['partially_pay', 'partially_paid']; + yield ['cancel', 'cancelled']; + } + + public function availableTransitionsForAuthorizedState(): iterable + { + yield ['cancel', 'cancelled']; + yield ['pay', 'paid']; + } + + public function availableTransitionsForPartiallyPaidState(): iterable + { + yield ['partially_pay', 'partially_paid']; + yield ['pay', 'paid']; + yield ['partially_refund', 'partially_refunded']; + yield ['refund', 'refunded']; + } + + public function availableTransitionsForPaidState(): iterable + { + yield ['partially_refund', 'partially_refunded']; + yield ['refund', 'refunded']; + } + + public function availableTransitionsForPartiallyRefundedState(): iterable + { + yield ['partially_refund', 'partially_refunded']; + yield ['refund', 'refunded']; + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius_abstraction.state_machine.adapter.symfony_workflow'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderShippingWorkflowTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderShippingWorkflowTest.php new file mode 100644 index 0000000000..2f421cecaa --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderShippingWorkflowTest.php @@ -0,0 +1,64 @@ +getStateMachine(); + $subject = new Order(); + $stateMachine->apply($subject, 'sylius_order_shipping', 'request_shipping'); + $stateMachine->apply($subject, 'sylius_order_shipping', $transition); + + $this->assertSame($expectedStatus, $subject->getShippingState()); + } + + /** @test */ + public function it_applies_ship_transition_if_order_is_partially_shipped(): void + { + $stateMachine = $this->getStateMachine(); + $subject = new Order(); + $stateMachine->apply($subject, 'sylius_order_shipping', 'request_shipping'); + $stateMachine->apply($subject, 'sylius_order_shipping', 'partially_ship'); + + $this->assertSame(OrderShippingStates::STATE_PARTIALLY_SHIPPED, $subject->getShippingState()); + + $stateMachine->apply($subject, 'sylius_order_shipping', 'ship'); + + $this->assertSame(OrderShippingStates::STATE_SHIPPED, $subject->getShippingState()); + } + + public function availableTransitionsFromReadyState(): iterable + { + yield ['cancel', 'cancelled']; + yield ['partially_ship', 'partially_shipped']; + yield ['ship', 'shipped']; + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius_abstraction.state_machine.adapter.symfony_workflow'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderWorkflowTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderWorkflowTest.php new file mode 100644 index 0000000000..9e4d968a37 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/OrderWorkflowTest.php @@ -0,0 +1,80 @@ +createMock(RepositoryInterface::class); + $sequenceRepository + ->method('findOneBy') + ->willReturn(null) + ; + + self::getContainer()->set('sylius.repository.order_sequence', $sequenceRepository); + + $channel = $this->createMock(ChannelInterface::class); + $customer = $this->createMock(Customer::class); + + $order = new Order(); + $order->setChannel($channel); + $order->setCustomer($customer); + + $this->order = $order; + } + + /** + * @test + * + * @dataProvider availableTransitionsForOrder + */ + public function it_applies_all_available_transitions_for_order( + string $initialState, + string $transition, + string $expectedState, + ): void { + $stateMachine = $this->getStateMachine(); + $order = $this->order; + + $stateMachine->apply($order, OrderTransitions::GRAPH, $initialState); + $stateMachine->apply($order, OrderTransitions::GRAPH, $transition); + + $this->assertSame($expectedState, $order->getState()); + } + + public function availableTransitionsForOrder(): iterable + { + yield [OrderTransitions::TRANSITION_CREATE, OrderTransitions::TRANSITION_CANCEL, OrderInterface::STATE_CANCELLED]; + yield [OrderTransitions::TRANSITION_CREATE, OrderTransitions::TRANSITION_FULFILL, OrderInterface::STATE_FULFILLED]; + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius_abstraction.state_machine.adapter.symfony_workflow'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/PaymentWorkflowTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/PaymentWorkflowTest.php new file mode 100644 index 0000000000..2fb9b8800e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/PaymentWorkflowTest.php @@ -0,0 +1,82 @@ +order = new Order(); + + $this->payment = new Payment(); + $this->payment->setOrder($this->order); + } + + /** + * @test + * + * @dataProvider availableTransitions + */ + public function it_applies_all_available_transitions( + string $fromState, + string $transition, + string $toState, + ): void { + $this->payment->setState($fromState); + + $stateMachine = $this->getStateMachine(); + $stateMachine->apply($this->payment, PaymentTransitions::GRAPH, $transition); + + $this->assertSame($toState, $this->payment->getState()); + } + + public function availableTransitions(): iterable + { + yield [PaymentInterface::STATE_CART, PaymentTransitions::TRANSITION_CREATE, PaymentInterface::STATE_NEW]; + yield [PaymentInterface::STATE_NEW, PaymentTransitions::TRANSITION_PROCESS, PaymentInterface::STATE_PROCESSING]; + yield [PaymentInterface::STATE_NEW, PaymentTransitions::TRANSITION_AUTHORIZE, PaymentInterface::STATE_AUTHORIZED]; + yield [PaymentInterface::STATE_PROCESSING, PaymentTransitions::TRANSITION_AUTHORIZE, PaymentInterface::STATE_AUTHORIZED]; + yield [PaymentInterface::STATE_NEW, PaymentTransitions::TRANSITION_COMPLETE, PaymentInterface::STATE_COMPLETED]; + yield [PaymentInterface::STATE_PROCESSING, PaymentTransitions::TRANSITION_COMPLETE, PaymentInterface::STATE_COMPLETED]; + yield [PaymentInterface::STATE_AUTHORIZED, PaymentTransitions::TRANSITION_COMPLETE, PaymentInterface::STATE_COMPLETED]; + yield [PaymentInterface::STATE_NEW, PaymentTransitions::TRANSITION_FAIL, PaymentInterface::STATE_FAILED]; + yield [PaymentInterface::STATE_PROCESSING, PaymentTransitions::TRANSITION_FAIL, PaymentInterface::STATE_FAILED]; + yield [PaymentInterface::STATE_AUTHORIZED, PaymentTransitions::TRANSITION_FAIL, PaymentInterface::STATE_FAILED]; + yield [PaymentInterface::STATE_NEW, PaymentTransitions::TRANSITION_CANCEL, PaymentInterface::STATE_CANCELLED]; + yield [PaymentInterface::STATE_PROCESSING, PaymentTransitions::TRANSITION_CANCEL, PaymentInterface::STATE_CANCELLED]; + yield [PaymentInterface::STATE_AUTHORIZED, PaymentTransitions::TRANSITION_CANCEL, PaymentInterface::STATE_CANCELLED]; + yield [PaymentInterface::STATE_COMPLETED, PaymentTransitions::TRANSITION_REFUND, PaymentInterface::STATE_REFUNDED]; + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius_abstraction.state_machine.adapter.symfony_workflow'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/ProductReviewWorkflowTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/ProductReviewWorkflowTest.php new file mode 100644 index 0000000000..fbc6e9ef41 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/ProductReviewWorkflowTest.php @@ -0,0 +1,46 @@ +getStateMachine(); + $subject = new ProductReview(); + $stateMachine->apply($subject, 'sylius_product_review', $transition); + + $this->assertSame($expectedStatus, $subject->getStatus()); + } + + public function availableTransitionsForNewStatus(): iterable + { + yield ['accept', 'accepted']; + yield ['reject', 'rejected']; + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius_abstraction.state_machine.adapter.symfony_workflow'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/ShipmentWorkflowTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/ShipmentWorkflowTest.php new file mode 100644 index 0000000000..76c9927be0 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/ShipmentWorkflowTest.php @@ -0,0 +1,66 @@ +order = new Order(); + + $this->shipment = new Shipment(); + $this->shipment->setOrder($this->order); + } + + /** + * @test + * + * @dataProvider availableTransitionsFromReadyState + */ + public function it_applies_all_available_transitions_for_create_state(string $transition, string $expectedStatus): void + { + $stateMachine = $this->getStateMachine(); + $stateMachine->apply($this->order, 'sylius_order_shipping', 'request_shipping'); + $stateMachine->apply($this->shipment, 'sylius_shipment', 'create'); + $stateMachine->apply($this->shipment, 'sylius_shipment', $transition); + + $this->assertSame($expectedStatus, $this->shipment->getState()); + } + + public function availableTransitionsFromReadyState(): iterable + { + yield ['cancel', 'cancelled']; + yield ['ship', 'shipped']; + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius_abstraction.state_machine.adapter.symfony_workflow'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/StateMachineCompositeTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/StateMachineCompositeTest.php new file mode 100644 index 0000000000..f18d02f7be --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Functional/StateMachine/StateMachineCompositeTest.php @@ -0,0 +1,47 @@ +getStateMachine(); + + $subject = new BlogPost(); + + $this->assertTrue($stateMachine->can($subject, 'app_blog_post', 'publish')); + } + + /** @test */ + public function it_calls_a_method_on_a_configured_adapter_for_a_given_graph(): void + { + $stateMachine = $this->getStateMachine(); + + $subject = new Comment(); + + $this->assertTrue($stateMachine->can($subject, 'app_comment', 'post')); + } + + private function getStateMachine(): StateMachineInterface + { + return self::getContainer()->get('sylius_abstraction.state_machine.composite'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Installation/Setup/LocaleSetupTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Installation/Setup/LocaleSetupTest.php new file mode 100644 index 0000000000..7b4172937e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Installation/Setup/LocaleSetupTest.php @@ -0,0 +1,172 @@ +filesystem = new Filesystem(); + $this->localeRepository = $this->prophesize(RepositoryInterface::class); + $this->localeFactory = $this->prophesize(FactoryInterface::class); + $this->localeParameterFilePath = self::$kernel->getProjectDir() . '/var/temporary_services_file.yaml'; + $this->createTemporaryServicesFile(['parameters' => ['locale' => 'en_US']]); + + $this->localeSetup = new LocaleSetup( + $this->localeRepository->reveal(), + $this->localeFactory->reveal(), + 'en_US', + $this->filesystem, + $this->localeParameterFilePath, + ); + } + + /** @test */ + public function it_updates_locale_with_a_given_one_if_it_is_different_than_default_one(): void + { + $questionHelper = $this->prophesize(QuestionHelper::class); + $questionHelper->ask(Argument::cetera())->willReturn('fr_FR'); + $locale = $this->prophesize(LocaleInterface::class); + + $this->localeRepository->findOneBy(['code' => 'fr_FR'])->willReturn(null); + $this->localeFactory->createNew()->willReturn($locale->reveal()); + $locale->setCode('fr_FR')->shouldBeCalled(); + $this->localeRepository->add($locale->reveal())->shouldBeCalled(); + + $this->localeSetup->setup( + $this->prophesize(InputInterface::class)->reveal(), + $this->prophesize(OutputInterface::class)->reveal(), + $questionHelper->reveal(), + ); + + $fileContent = Yaml::parseFile($this->localeParameterFilePath); + $this->assertEquals('fr_FR', $fileContent['parameters']['locale']); + + unlink($this->localeParameterFilePath); + } + + /** @test */ + public function it_does_not_update_locale_with_existing_locale(): void + { + $questionHelper = $this->prophesize(QuestionHelper::class); + $questionHelper->ask(Argument::cetera())->willReturn('en_US'); + $locale = $this->prophesize(LocaleInterface::class); + + $this->localeRepository->findOneBy(['code' => 'en_US'])->willReturn($locale->reveal()); + $this->localeFactory->createNew()->shouldNotBeCalled(); + $locale->setCode('en_US')->shouldNotBeCalled(); + $this->localeRepository->add($locale->reveal())->shouldNotBeCalled(); + + $this->localeSetup->setup( + $this->prophesize(InputInterface::class)->reveal(), + $this->prophesize(OutputInterface::class)->reveal(), + $questionHelper->reveal(), + ); + + $this->assertEquals('en_US', Yaml::parseFile($this->localeParameterFilePath)['parameters']['locale']); + + unlink($this->localeParameterFilePath); + } + + /** @test */ + public function it_shows_message_at_output_when_the_file_does_not_exists_or_path_is_null(): void + { + unlink($this->localeParameterFilePath); + + $questionHelper = $this->prophesize(QuestionHelper::class); + $questionHelper->ask(Argument::cetera())->willReturn('fr_FR'); + $locale = $this->prophesize(LocaleInterface::class); + + $this->localeRepository->findOneBy(['code' => 'fr_FR'])->willReturn(null); + $this->localeFactory->createNew()->willReturn($locale->reveal()); + $locale->setCode('fr_FR')->shouldBeCalled(); + $this->localeRepository->add($locale->reveal())->shouldBeCalled(); + + $output = $this->prophesize(OutputInterface::class); + $output->writeln('Adding French Language.')->shouldBeCalled(); + $output->writeln('Adding fr_FR locale.')->shouldBeCalled(); + $output->writeln('You may also need to add this locale into config/parameters.yaml configuration.')->shouldBeCalled(); + + $this->localeSetup->setup( + $this->prophesize(InputInterface::class)->reveal(), + $output->reveal(), + $questionHelper->reveal(), + ); + } + + /** @test */ + public function it_shows_message_at_output_when_the_filesystem_argument_is_null(): void + { + $this->localeSetup = new LocaleSetup( + $this->localeRepository->reveal(), + $this->localeFactory->reveal(), + 'en_US', + null, + $this->localeParameterFilePath, + ); + + $questionHelper = $this->prophesize(QuestionHelper::class); + $questionHelper->ask(Argument::cetera())->willReturn('fr_FR'); + $locale = $this->prophesize(LocaleInterface::class); + + $this->localeRepository->findOneBy(['code' => 'fr_FR'])->willReturn(null); + $this->localeFactory->createNew()->willReturn($locale->reveal()); + $locale->setCode('fr_FR')->shouldBeCalled(); + $this->localeRepository->add($locale->reveal())->shouldBeCalled(); + + $output = $this->prophesize(OutputInterface::class); + $output->writeln('Adding French Language.')->shouldBeCalled(); + $output->writeln('Adding fr_FR locale.')->shouldBeCalled(); + $output->writeln('You may also need to add this locale into config/parameters.yaml configuration.')->shouldBeCalled(); + + $this->localeSetup->setup( + $this->prophesize(InputInterface::class)->reveal(), + $output->reveal(), + $questionHelper->reveal(), + ); + } + + private function createTemporaryServicesFile(array $parameters): void + { + $content = Yaml::dump($parameters); + file_put_contents($this->localeParameterFilePath, $content); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Mailer/AccountRegistrationEmailManagerTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/AccountRegistrationEmailManagerTest.php new file mode 100644 index 0000000000..7353bf451a --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/AccountRegistrationEmailManagerTest.php @@ -0,0 +1,67 @@ +get('translator'); + + /** @var AccountRegistrationEmailManagerInterface $accountRegistrationEmailManager */ + $accountRegistrationEmailManager = $container->get(AccountRegistrationEmailManagerInterface::class); + + /** @var UserInterface|ObjectProphecy $user */ + $user = $this->prophesize(UserInterface::class); + $user->getUsername()->willReturn('username'); + $user->getEmail()->willReturn('customer@example.com'); + $user->getEmailVerificationToken()->willReturn('token'); + /** @var ChannelInterface|ObjectProphecy $channel */ + $channel = $this->prophesize(ChannelInterface::class); + + $accountRegistrationEmailManager->sendAccountRegistrationEmail( + $user->reveal(), + $channel->reveal(), + 'en_US', + ); + + self::assertEmailCount(1); + $email = self::getMailerMessage(); + self::assertEmailAddressContains($email, 'To', 'customer@example.com'); + self::assertEmailHtmlBodyContains( + $email, + $translator->trans('sylius.email.user_registration.start_shopping', [], null, 'en_US'), + ); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Mailer/AccountVerificationEmailManagerTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/AccountVerificationEmailManagerTest.php new file mode 100644 index 0000000000..5fb5a73ac5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/AccountVerificationEmailManagerTest.php @@ -0,0 +1,66 @@ +markTestSkipped('This test should be executed only outside of test_with_swiftmailer environment'); + } + + $container = self::getContainer(); + + /** @var TranslatorInterface $translator */ + $translator = $container->get('translator'); + + $accountVerificationEmailManager = $container->get(AccountVerificationEmailManagerInterface::class); + + /** @var UserInterface|ObjectProphecy $user */ + $user = $this->prophesize(UserInterface::class); + $user->getUsername()->willReturn('username'); + $user->getEmail()->willReturn('customer@example.com'); + $user->getEmailVerificationToken()->willReturn('token'); + /** @var ChannelInterface|ObjectProphecy $channel */ + $channel = $this->prophesize(ChannelInterface::class); + + $accountVerificationEmailManager->sendAccountVerificationEmail( + $user->reveal(), + $channel->reveal(), + 'en_US', + ); + + self::assertEmailCount(1); + $email = self::getMailerMessage(); + self::assertEmailAddressContains($email, 'To', 'customer@example.com'); + self::assertEmailHtmlBodyContains( + $email, + $translator->trans('sylius.email.user.account_verification.message', [], null, 'en_US'), + ); + } +} diff --git a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendContactRequestHandlerTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/ContactEmailManagerTest.php similarity index 64% rename from src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendContactRequestHandlerTest.php rename to src/Sylius/Bundle/CoreBundle/Tests/Mailer/ContactEmailManagerTest.php index 54bfab844b..d3179e1bbe 100644 --- a/src/Sylius/Bundle/ApiBundle/Tests/CommandHandler/SendContactRequestHandlerTest.php +++ b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/ContactEmailManagerTest.php @@ -11,25 +11,26 @@ declare(strict_types=1); -namespace Sylius\Bundle\ApiBundle\Tests\CommandHandler; +namespace Sylius\Bundle\CoreBundle\Tests\Mailer; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Sylius\Bundle\ApiBundle\Command\SendContactRequest; -use Sylius\Bundle\ApiBundle\CommandHandler\SendContactRequestHandler; +use Sylius\Bundle\CoreBundle\Mailer\ContactEmailManager; use Sylius\Component\Channel\Repository\ChannelRepositoryInterface; use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Test\SwiftmailerAssertionTrait; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Symfony\Bundle\FrameworkBundle\Test\MailerAssertionsTrait; use Symfony\Contracts\Translation\TranslatorInterface; -final class SendContactRequestHandlerTest extends KernelTestCase +final class ContactEmailManagerTest extends KernelTestCase { - use MailerAssertionsTrait; + use ProphecyTrait; + use SwiftmailerAssertionTrait; /** @test */ public function it_sends_contact_request(): void { - if ($this->isItSwiftmailerTestEnv()) { + if (self::isSwiftmailerTestEnv()) { $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); } @@ -45,32 +46,23 @@ final class SendContactRequestHandlerTest extends KernelTestCase /** @var ChannelInterface|ObjectProphecy $channel */ $channel = $this->prophesize(ChannelInterface::class); - $sendContactRequest = new SendContactRequest('shopUser@example.com', 'hello!'); - $sendContactRequest->setChannelCode('CHANNEL_CODE'); - $sendContactRequest->setLocaleCode('en_US'); - $channel->getHostname()->willReturn('Channel.host'); $channel->getContactEmail()->willReturn('shop@example.com'); $channelRepository->findOneByCode('CHANNEL_CODE')->willReturn($channel->reveal()); - $sendContactEmailHandler = new SendContactRequestHandler( - $emailSender, - $channelRepository->reveal(), - ); + $contactEmailManager = new ContactEmailManager($emailSender); - $sendContactEmailHandler($sendContactRequest); + $contactEmailManager->sendContactRequest( + ['email' => 'shop@example.com', 'message' => 'Hello contact request!'], + ['shop@example.com'], + $channel->reveal(), + 'en_US', + ); self::assertEmailCount(1); $email = self::getMailerMessage(); self::assertEmailAddressContains($email, 'To', 'shop@example.com'); self::assertEmailHtmlBodyContains($email, $translator->trans('sylius.email.contact_request.content', [], null, 'en_US')); } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } } diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Mailer/OrderEmailManagerTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/OrderEmailManagerTest.php index 6a4df6bfa5..6c421dc151 100644 --- a/src/Sylius/Bundle/CoreBundle/Tests/Mailer/OrderEmailManagerTest.php +++ b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/OrderEmailManagerTest.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Tests\Mailer; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; use Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface; use Sylius\Component\Core\Model\ChannelInterface; @@ -25,6 +26,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; final class OrderEmailManagerTest extends KernelTestCase { + use ProphecyTrait; use SwiftmailerAssertionTrait; private const RECIPIENT_EMAIL = 'test@example.com'; @@ -36,7 +38,7 @@ final class OrderEmailManagerTest extends KernelTestCase /** @test */ public function it_sends_order_confirmation_email_with_symfony_mailer_if_swift_mailer_is_not_present(): void { - if ($this->isItSwiftmailerTestEnv()) { + if (self::isSwiftmailerTestEnv()) { $this->markTestSkipped('This test should be executed only outside of test_with_swiftmailer environment'); } @@ -80,7 +82,7 @@ final class OrderEmailManagerTest extends KernelTestCase /** @test */ public function it_sends_order_confirmation_email_with_swift_mailer_by_default_if_is_present(): void { - if (!$this->isItSwiftmailerTestEnv()) { + if (!self::isSwiftmailerTestEnv()) { $this->markTestSkipped('This test should be executed only in test_with_swiftmailer environment'); } @@ -124,11 +126,4 @@ final class OrderEmailManagerTest extends KernelTestCase self::RECIPIENT_EMAIL, ); } - - private function isItSwiftmailerTestEnv(): bool - { - $env = self::getContainer()->getParameter('kernel.environment'); - - return $env === 'test_with_swiftmailer'; - } } diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Mailer/ResetPasswordEmailManagerTest.php b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/ResetPasswordEmailManagerTest.php new file mode 100644 index 0000000000..abe53b2b8b --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Mailer/ResetPasswordEmailManagerTest.php @@ -0,0 +1,64 @@ +resetPasswordEmailManager = self::getContainer()->get(ResetPasswordEmailManagerInterface::class); + + $this->translator = self::getContainer()->get('translator'); + + $this->adminUser = new AdminUser(); + $this->adminUser->setEmail(self::RECIPIENT_EMAIL); + } + + /** @test */ + public function it_sends_admin_reset_password_email(): void + { + if (self::isSwiftmailerTestEnv()) { + $this->markTestSkipped('Test is relevant only for the environment without swiftmailer'); + } + + $this->resetPasswordEmailManager->sendAdminResetPasswordEmail($this->adminUser, 'en_US'); + + self::assertEmailCount(1); + $email = self::getMailerMessage(); + self::assertEmailAddressContains($email, 'To', self::RECIPIENT_EMAIL); + self::assertEmailHtmlBodyContains( + $email, + $this->translator->trans( + id: 'sylius.email.admin_password_reset.to_reset_your_password_token', + locale: 'en_US', + ), + ); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Tests/Stub/CatalogPromotionApplicatorCriteriaStub.php b/src/Sylius/Bundle/CoreBundle/Tests/Stub/CatalogPromotionApplicatorCriteriaStub.php new file mode 100644 index 0000000000..cd31f993be --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Tests/Stub/CatalogPromotionApplicatorCriteriaStub.php @@ -0,0 +1,27 @@ +channelContext->getChannel(); - $themeName = $channel->getThemeName(); - - if (null === $themeName) { + if (false === $this->theme) { + try { + /** @var ChannelInterface $channel */ + $channel = $this->channelContext->getChannel(); + $themeName = $channel->getThemeName(); + $this->theme = null === $themeName + ? null + : $this->themeRepository->findOneByName($themeName) + ; + } catch (ChannelNotFoundException|\Exception) { return null; } - - return $this->themeRepository->findOneByName($themeName); - } catch (ChannelNotFoundException) { - return null; - } catch (\Exception) { - return null; } + + return $this->theme; } } diff --git a/src/Sylius/Bundle/CoreBundle/Twig/PriceExtension.php b/src/Sylius/Bundle/CoreBundle/Twig/PriceExtension.php index 101950eb5b..e0afdcb6c8 100644 --- a/src/Sylius/Bundle/CoreBundle/Twig/PriceExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Twig/PriceExtension.php @@ -29,6 +29,8 @@ final class PriceExtension extends AbstractExtension new TwigFilter('sylius_calculate_price', [$this->helper, 'getPrice']), new TwigFilter('sylius_calculate_original_price', [$this->helper, 'getOriginalPrice']), new TwigFilter('sylius_has_discount', [$this->helper, 'hasDiscount']), + new TwigFilter('sylius_has_lowest_price', [$this->helper, 'hasLowestPriceBeforeDiscount']), + new TwigFilter('sylius_calculate_lowest_price', [$this->helper, 'getLowestPriceBeforeDiscount']), ]; } } diff --git a/src/Sylius/Bundle/CoreBundle/Twig/ProductVariantsMapExtension.php b/src/Sylius/Bundle/CoreBundle/Twig/ProductVariantsMapExtension.php new file mode 100644 index 0000000000..705a98cc1f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Twig/ProductVariantsMapExtension.php @@ -0,0 +1,32 @@ +productVariantsMapProvider, 'provide']), + ]; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Twig/ProductVariantsPricesExtension.php b/src/Sylius/Bundle/CoreBundle/Twig/ProductVariantsPricesExtension.php index c97904e045..defc0f09db 100644 --- a/src/Sylius/Bundle/CoreBundle/Twig/ProductVariantsPricesExtension.php +++ b/src/Sylius/Bundle/CoreBundle/Twig/ProductVariantsPricesExtension.php @@ -17,10 +17,18 @@ use Sylius\Bundle\CoreBundle\Templating\Helper\ProductVariantsPricesHelper; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see ProductVariantsMapExtension} instead. */ final class ProductVariantsPricesExtension extends AbstractExtension { public function __construct(private ProductVariantsPricesHelper $productVariantsPricesHelper) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + self::class, + ProductVariantsMapExtension::class, + ); } public function getFunctions(): array diff --git a/src/Sylius/Bundle/CoreBundle/Twig/StateMachineExtension.php b/src/Sylius/Bundle/CoreBundle/Twig/StateMachineExtension.php new file mode 100644 index 0000000000..5ce89d8767 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Twig/StateMachineExtension.php @@ -0,0 +1,36 @@ + + */ + public function getFunctions(): array + { + return [ + new TwigFunction('sylius_state_machine_can', [$this->stateMachine, 'can']), + new TwigFunction('sylius_state_machine_get_enabled_transitions', [$this->stateMachine, 'getEnabledTransitions']), + ]; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CartItemAvailabilityValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CartItemAvailabilityValidator.php index 916a332cda..f0cc0ecc5a 100644 --- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CartItemAvailabilityValidator.php +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CartItemAvailabilityValidator.php @@ -27,7 +27,7 @@ final class CartItemAvailabilityValidator extends ConstraintValidator { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var AddToCartCommandInterface $value */ Assert::isInstanceOf($value, AddToCartCommandInterface::class); diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelCodeCollection.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelCodeCollection.php new file mode 100644 index 0000000000..e01adccf84 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelCodeCollection.php @@ -0,0 +1,39 @@ + */ + public array $constraints = []; + + public bool $allowExtraFields = false; + + public bool $allowMissingFields = false; + + public ?string $channelAwarePropertyPath = null; + + public ?string $extraFieldsMessage = null; + + public ?string $missingFieldsMessage = null; + + public bool $validateAgainstAllChannels = false; + + public function validatedBy(): string + { + return 'sylius_channel_code_collection'; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelCodeCollectionValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelCodeCollectionValidator.php new file mode 100644 index 0000000000..6b8fa4a64f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelCodeCollectionValidator.php @@ -0,0 +1,98 @@ + $channelRepository */ + public function __construct( + private ChannelRepositoryInterface $channelRepository, + private PropertyAccessorInterface $propertyAccessor, + ) { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof ChannelCodeCollection) { + throw new UnexpectedTypeException($constraint, ChannelCodeCollection::class); + } + + if (!is_array($value)) { + throw new UnexpectedValueException($value, 'array'); + } + + if ($constraint->validateAgainstAllChannels) { + $this->validateInChannelCollection($value, $this->channelRepository->findAll(), $constraint); + + return; + } + + $object = $this->context->getObject(); + + if (null !== $constraint->channelAwarePropertyPath) { + $object = $this->propertyAccessor->getValue($object, $constraint->channelAwarePropertyPath); + } + + if (!$object instanceof ChannelsAwareInterface) { + throw new \LogicException(sprintf( + 'The validated object needs to implement the %s interface when option `validateAgainstAllChannels` is set to false.', + ChannelsAwareInterface::class, + )); + } + + $this->validateInChannelCollection($value, $object->getChannels()->toArray(), $constraint); + } + + /** + * @param array> $value + * @param array $channels + */ + private function validateInChannelCollection( + array $value, + array $channels, + ChannelCodeCollection $constraint, + ): void { + $fields = []; + foreach ($channels as $channel) { + $fields[$channel->getCode()] = $constraint->constraints; + } + if ([] === $fields) { + return; + } + + $collection = new Collection( + $fields, + $constraint->groups, + $constraint->payload, + $constraint->allowExtraFields, + $constraint->allowMissingFields, + $constraint->extraFieldsMessage, + $constraint->missingFieldsMessage, + ); + + $validator = $this->context->getValidator()->inContext($this->context); + $validator->validate($value, $collection, $constraint->groups); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelDefaultLocaleEnabledValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelDefaultLocaleEnabledValidator.php index 0d5d84127f..af3e6c3d4a 100644 --- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelDefaultLocaleEnabledValidator.php +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ChannelDefaultLocaleEnabledValidator.php @@ -20,7 +20,7 @@ use Webmozart\Assert\Assert; final class ChannelDefaultLocaleEnabledValidator extends ConstraintValidator { - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var ChannelDefaultLocaleEnabled $constraint */ Assert::isInstanceOf($constraint, ChannelDefaultLocaleEnabled::class); diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CountryCodeExists.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CountryCodeExists.php new file mode 100644 index 0000000000..2caf57af94 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CountryCodeExists.php @@ -0,0 +1,31 @@ + $countryRepository */ + public function __construct(private RepositoryInterface $countryRepository) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof CountryCodeExists) { + throw new UnexpectedTypeException($constraint, CountryCodeExists::class); + } + + if (empty($value)) { + return; + } + + if ($this->countryRepository->findOneBy(['code' => $value]) === null) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ code }}', $value) + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CustomerGroupCodeExists.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CustomerGroupCodeExists.php new file mode 100644 index 0000000000..d053ade824 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/CustomerGroupCodeExists.php @@ -0,0 +1,31 @@ + $customerGroupRepository */ + public function __construct(private CustomerGroupRepositoryInterface $customerGroupRepository) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof CustomerGroupCodeExists) { + throw new UnexpectedTypeException($constraint, CustomerGroupCodeExists::class); + } + + if (empty($value)) { + return; + } + + if ($this->customerGroupRepository->findOneBy(['code' => $value]) === null) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ code }}', $value) + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ExistingChannelCode.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ExistingChannelCode.php new file mode 100644 index 0000000000..fbea9c81c5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ExistingChannelCode.php @@ -0,0 +1,31 @@ +channelRepository->findOneByCode($value) === null) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ channelCode }}', $value) + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasAllPricesDefinedValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasAllPricesDefinedValidator.php index e374d14e19..aa2a3fcfa3 100644 --- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasAllPricesDefinedValidator.php +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasAllPricesDefinedValidator.php @@ -13,19 +13,31 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Validator\Constraints; +use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ChannelPricingInterface; +use Sylius\Component\Core\Model\ProductInterface; +use Sylius\Component\Core\Model\ProductVariantInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; final class HasAllPricesDefinedValidator extends ConstraintValidator { - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { + Assert::isInstanceOf($value, ProductVariantInterface::class); Assert::isInstanceOf($constraint, HasAllPricesDefined::class); - $channels = $value->getProduct()->getChannels(); + /** @var ProductInterface|null $product */ + $product = $value->getProduct(); + if ($product === null) { + return; + } + + $channels = $product->getChannels(); + + /** @var ChannelInterface $channel */ foreach ($channels as $channel) { /** @var ChannelPricingInterface|null $channelPricing */ $channelPricing = $value->getChannelPricingForChannel($channel); diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasAllVariantPricesDefinedValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasAllVariantPricesDefinedValidator.php index 215346a43c..9fc6070153 100644 --- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasAllVariantPricesDefinedValidator.php +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasAllVariantPricesDefinedValidator.php @@ -23,7 +23,7 @@ use Webmozart\Assert\Assert; final class HasAllVariantPricesDefinedValidator extends ConstraintValidator { - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var ProductInterface $value */ Assert::isInstanceOf($value, ProductInterface::class); diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasEnabledEntityValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasEnabledEntityValidator.php index fb45e946b3..3b69735cc0 100644 --- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasEnabledEntityValidator.php +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/HasEnabledEntityValidator.php @@ -14,10 +14,9 @@ declare(strict_types=1); namespace Sylius\Bundle\CoreBundle\Validator\Constraints; use Doctrine\Persistence\ManagerRegistry; -use Doctrine\Persistence\Mapping\ClassMetadata; use Doctrine\Persistence\ObjectManager; use Symfony\Component\PropertyAccess\PropertyAccess; -use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; @@ -25,21 +24,27 @@ use Webmozart\Assert\Assert; final class HasEnabledEntityValidator extends ConstraintValidator { - private PropertyAccessor $accessor; + public function __construct( + private ManagerRegistry $registry, + private ?PropertyAccessorInterface $accessor = null, + ) { + if (null === $this->accessor) { + trigger_deprecation( + 'sylius/core-bundle', + '1.13', + 'Not passing a PropertyAccessorInterface as the second constructor argument for %s is deprecated and will be required in Sylius 2.0.', + self::class, + ); - public function __construct(private ManagerRegistry $registry) - { - $this->accessor = PropertyAccess::createPropertyAccessor(); + $this->accessor = PropertyAccess::createPropertyAccessor(); + } } - /** - * @throws \InvalidArgumentException - * @throws ConstraintDefinitionException - */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var HasEnabledEntity $constraint */ Assert::isInstanceOf($constraint, HasEnabledEntity::class); + Assert::object($value, 'This validator can only be used with objects.'); $enabled = $this->accessor->getValue($value, $constraint->enabledPath); @@ -51,10 +56,8 @@ final class HasEnabledEntityValidator extends ConstraintValidator $this->ensureEntityHasProvidedEnabledField($objectManager, $value, $constraint->enabledPath); - $criteria = [$constraint->enabledPath => true]; - $repository = $objectManager->getRepository($value::class); - $results = $repository->{$constraint->repositoryMethod}($criteria); + $results = $repository->{$constraint->repositoryMethod}([$constraint->enabledPath => true]); /* If the result is a MongoCursor, it must be advanced to the first * element. Rewinding should have no ill effect if $result is another @@ -67,7 +70,7 @@ final class HasEnabledEntityValidator extends ConstraintValidator } if ($this->isLastEnabledEntity($results, $value)) { - $errorPath = null !== $constraint->errorPath ? $constraint->errorPath : $constraint->enabledPath; + $errorPath = $constraint->errorPath ?? $constraint->enabledPath; $this->context->buildViolation($constraint->message)->atPath($errorPath)->addViolation(); } @@ -76,19 +79,16 @@ final class HasEnabledEntityValidator extends ConstraintValidator /** * If no entity matched the query criteria or a single entity matched, which is the same as the entity being * validated, the entity is the last enabled entity available. - * - * @param object $entity */ - private function isLastEnabledEntity(array|\Iterator $result, $entity): bool + private function isLastEnabledEntity(array|\Iterator $result, object $entity): bool { - return !\is_countable($result) || 0 === count($result) || - (1 === count($result) && $entity === ($result instanceof \Iterator ? $result->current() : current($result))); + return + !\is_countable($result) || 0 === count($result) || + (1 === count($result) && $entity === ($result instanceof \Iterator ? $result->current() : current($result))) + ; } - /** - * @param object $entity - */ - private function getProperObjectManager(?string $manager, $entity): ?ObjectManager + private function getProperObjectManager(?string $manager, object $entity): ObjectManager { if ($manager) { $objectManager = $this->registry->getManager($manager); @@ -114,19 +114,19 @@ final class HasEnabledEntityValidator extends ConstraintValidator */ private function validateObjectManager(?ObjectManager $objectManager, string $exceptionMessage): void { - if (!$objectManager) { + if (null === $objectManager) { throw new ConstraintDefinitionException($exceptionMessage); } } /** - * @param object $entity - * * @throws ConstraintDefinitionException */ - private function ensureEntityHasProvidedEnabledField(ObjectManager $objectManager, $entity, string $enabledPropertyPath): void - { - /** @var ClassMetadata $class */ + private function ensureEntityHasProvidedEnabledField( + ObjectManager $objectManager, + object $entity, + string $enabledPropertyPath, + ): void { $class = $objectManager->getClassMetadata($entity::class); if (!$class->hasField($enabledPropertyPath) && !$class->hasAssociation($enabledPropertyPath)) { diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/LocalesAwareValidAttributeValueValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/LocalesAwareValidAttributeValueValidator.php index a1244e7b96..9fcc32ce24 100644 --- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/LocalesAwareValidAttributeValueValidator.php +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/LocalesAwareValidAttributeValueValidator.php @@ -32,7 +32,7 @@ final class LocalesAwareValidAttributeValueValidator extends ConstraintValidator /** * @throws \InvalidArgumentException */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { Assert::isInstanceOf($value, AttributeValueInterface::class); diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderPaymentMethodEligibilityValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderPaymentMethodEligibilityValidator.php index 05ed130a04..28870e086c 100644 --- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderPaymentMethodEligibilityValidator.php +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderPaymentMethodEligibilityValidator.php @@ -23,7 +23,7 @@ final class OrderPaymentMethodEligibilityValidator extends ConstraintValidator /** * @throws \InvalidArgumentException */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var OrderInterface $value */ Assert::isInstanceOf($value, OrderInterface::class); diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderProductEligibilityValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderProductEligibilityValidator.php index 5f00ea1b13..5d34c90e44 100644 --- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderProductEligibilityValidator.php +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/OrderProductEligibilityValidator.php @@ -24,7 +24,7 @@ final class OrderProductEligibilityValidator extends ConstraintValidator /** * @throws \InvalidArgumentException */ - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var OrderInterface $value */ Assert::isInstanceOf($value, OrderInterface::class); diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductCodeExists.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductCodeExists.php new file mode 100644 index 0000000000..d967e6195d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductCodeExists.php @@ -0,0 +1,31 @@ + $productRepository */ + public function __construct(private ProductRepositoryInterface $productRepository) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof ProductCodeExists) { + throw new UnexpectedTypeException($constraint, ProductCodeExists::class); + } + + if (empty($value)) { + return; + } + + if ($this->productRepository->findOneByCode($value) === null) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ code }}', $value) + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductImageVariantsBelongToOwner.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductImageVariantsBelongToOwner.php new file mode 100644 index 0000000000..c61dbe8fee --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductImageVariantsBelongToOwner.php @@ -0,0 +1,31 @@ +getOwner(); + + foreach ($value->getProductVariants() as $productVariant) { + if (!$owner->hasVariant($productVariant)) { + $this->context->addViolation($constraint->message, [ + '%productVariantCode%' => $productVariant->getCode(), + '%ownerCode%' => $owner->getCode(), + ]); + } + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductVariantCodeExists.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductVariantCodeExists.php new file mode 100644 index 0000000000..763a0b698f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ProductVariantCodeExists.php @@ -0,0 +1,31 @@ + $productVariantRepository */ + public function __construct(private ProductVariantRepositoryInterface $productVariantRepository) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof ProductVariantCodeExists) { + throw new UnexpectedTypeException($constraint, ProductVariantCodeExists::class); + } + + if (null === $value || '' === $value) { + return; + } + + if ($this->productVariantRepository->findOneBy(['code' => $value]) === null) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ code }}', $value) + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/RegisteredUserValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/RegisteredUserValidator.php index e6d61284eb..df4932db6e 100644 --- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/RegisteredUserValidator.php +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/RegisteredUserValidator.php @@ -25,7 +25,7 @@ final class RegisteredUserValidator extends ConstraintValidator { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var CustomerInterface $value */ Assert::isInstanceOf($value, CustomerInterface::class); diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ResendOrderConfirmationEmailWithValidOrderState.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ResendOrderConfirmationEmailWithValidOrderState.php new file mode 100644 index 0000000000..664547e749 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ResendOrderConfirmationEmailWithValidOrderState.php @@ -0,0 +1,31 @@ + $orderRepository + * @param array $orderStatesToAllowResendingConfirmationEmail + */ + public function __construct( + private RepositoryInterface $orderRepository, + private array $orderStatesToAllowResendingConfirmationEmail, + ) { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$value instanceof ResendOrderConfirmationEmail) { + throw new UnexpectedTypeException($value, ResendOrderConfirmationEmail::class); + } + + if (!$constraint instanceof ResendOrderConfirmationEmailWithValidOrderState) { + throw new UnexpectedTypeException($constraint, ResendOrderConfirmationEmailWithValidOrderState::class); + } + + /** @var OrderInterface|null $order */ + $order = $this->orderRepository->findOneBy(['tokenValue' => $value->getOrderTokenValue()]); + if (null === $order) { + return; + } + + if (!in_array($order->getState(), $this->orderStatesToAllowResendingConfirmationEmail, true)) { + $this->context->addViolation( + $constraint->message, + ['%state%' => $order->getState()], + ); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ResendShipmentConfirmationEmailWithValidShipmentState.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ResendShipmentConfirmationEmailWithValidShipmentState.php new file mode 100644 index 0000000000..7d12d3b6ce --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/ResendShipmentConfirmationEmailWithValidShipmentState.php @@ -0,0 +1,31 @@ + $shipmentRepository + */ + public function __construct(private RepositoryInterface $shipmentRepository) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$value instanceof ResendShipmentConfirmationEmail) { + throw new UnexpectedTypeException($value, ResendShipmentConfirmationEmail::class); + } + + if (!$constraint instanceof ResendShipmentConfirmationEmailWithValidShipmentState) { + throw new UnexpectedTypeException($constraint, ResendShipmentConfirmationEmailWithValidShipmentState::class); + } + + /** @var ShipmentInterface|null $shipment */ + $shipment = $this->shipmentRepository->findOneBy(['id' => $value->getShipmentId()]); + if (null === $shipment) { + return; + } + + if ($shipment->getState() !== ShipmentInterface::STATE_SHIPPED) { + $this->context->addViolation( + $constraint->message, + ['%state%' => $shipment->getState()], + ); + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/TaxonCodeExists.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/TaxonCodeExists.php new file mode 100644 index 0000000000..2930c3bb47 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/TaxonCodeExists.php @@ -0,0 +1,31 @@ + $taxonRepository */ + public function __construct(private TaxonRepositoryInterface $taxonRepository) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof TaxonCodeExists) { + throw new UnexpectedTypeException($constraint, TaxonCodeExists::class); + } + + if (empty($value)) { + return; + } + + if ($this->taxonRepository->findOneBy(['code' => $value]) === null) { + $this->context->buildViolation($constraint->message) + ->setParameter('{{ code }}', $value) + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/TranslationForExistingLocales.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/TranslationForExistingLocales.php new file mode 100644 index 0000000000..bbc60a2778 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/TranslationForExistingLocales.php @@ -0,0 +1,31 @@ +localeRepository->findAll(); + + if (empty($locales)) { + return; + } + + $localeCodes = array_map(fn (LocaleInterface $locale) => $locale->getCode(), $locales); + + $translations = $value->getTranslations(); + + /** @var TranslationInterface $translation */ + foreach ($translations as $key => $translation) { + if (!in_array($translation->getLocale(), $localeCodes, true)) { + $this->context->buildViolation($constraint->message) + ->setParameter('%locales%', implode(', ', $localeCodes)) + ->atPath(sprintf('translations[%s]', $key)) + ->addViolation() + ; + } + } + } +} diff --git a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/UniqueReviewerEmailValidator.php b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/UniqueReviewerEmailValidator.php index 28d272216d..84cac89ba2 100644 --- a/src/Sylius/Bundle/CoreBundle/Validator/Constraints/UniqueReviewerEmailValidator.php +++ b/src/Sylius/Bundle/CoreBundle/Validator/Constraints/UniqueReviewerEmailValidator.php @@ -31,7 +31,7 @@ class UniqueReviewerEmailValidator extends ConstraintValidator ) { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var UniqueReviewerEmail $constraint */ Assert::isInstanceOf($constraint, UniqueReviewerEmail::class); diff --git a/src/Sylius/Bundle/CoreBundle/composer.json b/src/Sylius/Bundle/CoreBundle/composer.json index a4bb08f38d..7d40bd5b2c 100644 --- a/src/Sylius/Bundle/CoreBundle/composer.json +++ b/src/Sylius/Bundle/CoreBundle/composer.json @@ -26,64 +26,65 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "doctrine/doctrine-migrations-bundle": "^3.0.1", "egulias/email-validator": "^3.1", "fakerphp/faker": "^1.10", - "jms/serializer-bundle": "^4.0", + "jms/serializer-bundle": "^4.2", "knplabs/gaufrette": "^0.10 || ^0.11", "knplabs/knp-gaufrette-bundle": "^0.7 || ^0.8", "league/flysystem-bundle": "^2.4", - "liip/imagine-bundle": "^2.3", - "sonata-project/block-bundle": "^4.2", + "liip/imagine-bundle": "^2.10", + "nyholm/psr7": "^1.6", + "sonata-project/block-bundle": "^4.2 || ^5.0", "sylius-labs/association-hydrator": "^1.1 || ^1.2", "sylius-labs/doctrine-migrations-extra-bundle": "^0.1.4 || ^0.2", "sylius-labs/polyfill-symfony-framework-bundle": "^1.0 || ^1.1", - "sylius/addressing-bundle": "^1.12", - "sylius/attribute-bundle": "^1.12", - "sylius/channel-bundle": "^1.12", - "sylius/core": "^1.12", - "sylius/currency-bundle": "^1.12", - "sylius/customer-bundle": "^1.12", + "sylius/addressing-bundle": "^1.13", + "sylius/attribute-bundle": "^1.13", + "sylius/channel-bundle": "^1.13", + "sylius/core": "^1.13", + "sylius/currency-bundle": "^1.13", + "sylius/customer-bundle": "^1.13", "sylius/fixtures-bundle": "^1.7", "sylius/grid-bundle": "^1.11", - "sylius/inventory-bundle": "^1.12", - "sylius/locale-bundle": "^1.12", - "sylius/money-bundle": "^1.12", - "sylius/order-bundle": "^1.12", - "sylius/payment-bundle": "^1.12", - "sylius/payum-bundle": "^1.12", - "sylius/product-bundle": "^1.12", - "sylius/promotion-bundle": "^1.12", + "sylius/inventory-bundle": "^1.13", + "sylius/locale-bundle": "^1.13", + "sylius/money-bundle": "^1.13", + "sylius/order-bundle": "^1.13", + "sylius/payment-bundle": "^1.13", + "sylius/payum-bundle": "^1.13", + "sylius/product-bundle": "^1.13", + "sylius/promotion-bundle": "^1.13", "sylius/resource-bundle": "^1.9", - "sylius/review-bundle": "^1.12", - "sylius/shipping-bundle": "^1.12", - "sylius/taxation-bundle": "^1.12", - "sylius/taxonomy-bundle": "^1.12", + "sylius/review-bundle": "^1.13", + "sylius/shipping-bundle": "^1.13", + "sylius/state-machine-abstraction": "^1.13", + "sylius/taxation-bundle": "^1.13", + "sylius/taxonomy-bundle": "^1.13", "sylius/mailer-bundle": "^1.8 || ^2.0@beta", "sylius/theme-bundle": "^2.1.1 || ^2.3", - "sylius/ui-bundle": "^1.12", - "sylius/user-bundle": "^1.12", - "symfony/framework-bundle": "^5.4 || ^6.0", - "symfony/intl": "^5.4 || ^6.0", - "symfony/mailer": "^5.4 || ^6.0", - "symfony/messenger": "^5.4 || ^6.0", - "symfony/templating": "^5.4 || ^6.0", + "sylius/ui-bundle": "^1.13", + "sylius/user-bundle": "^1.13", + "symfony/framework-bundle": "^5.4.21 || ^6.4", + "symfony/intl": "^5.4.21 || ^6.4", + "symfony/mailer": "^5.4.21 || ^6.4", + "symfony/messenger": "^5.4.21 || ^6.4", + "symfony/templating": "^5.4.21 || ^6.4", "symfony/webpack-encore-bundle": "^1.15", + "symfony/workflow": "^5.4.21 || ^6.4", "winzou/state-machine-bundle": "^0.6" }, "require-dev": { - "doctrine/doctrine-bundle": "^1.12 || ^2.0", + "doctrine/doctrine-bundle": "^1.12 || ^2.3.1", "hwi/oauth-bundle": "^1.1 || ^2.0@beta", "matthiasnoback/symfony-config-test": "^4.2", "matthiasnoback/symfony-dependency-injection-test": "^4.2", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/dotenv": "^5.4 || ^6.0" - }, - "conflict": { - "symfony/doctrine-bridge": "4.4.20 || 5.2.4 || 5.2.5" + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/dotenv": "^5.4.21 || ^6.4" }, "config": { "allow-plugins": { @@ -93,10 +94,10 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" }, "symfony": { - "require": "^5.4" + "require": "^5.4.21" } }, "autoload": { diff --git a/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Announcer/CatalogPromotionRemovalAnnouncerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Announcer/CatalogPromotionRemovalAnnouncerSpec.php new file mode 100644 index 0000000000..f9e64def60 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Announcer/CatalogPromotionRemovalAnnouncerSpec.php @@ -0,0 +1,72 @@ +beConstructedWith($commandBus); + } + + function it_implements_catalog_promotion_removal_announcer_interface(): void + { + $this->shouldImplement(CatalogPromotionRemovalAnnouncerInterface::class); + } + + function it_dispatches_remove_catalog_promotion_command_on_enabled_catalog_promotion( + MessageBusInterface $commandBus, + CatalogPromotionInterface $catalogPromotion, + ): void { + $catalogPromotion->getCode()->willReturn('CATALOG_PROMOTION_CODE'); + $catalogPromotion->isEnabled()->willReturn(true); + + $updateCatalogPromotionStateCommand = new UpdateCatalogPromotionState('CATALOG_PROMOTION_CODE'); + $disableCatalogPromotionCommand = new DisableCatalogPromotion('CATALOG_PROMOTION_CODE'); + $removeCatalogPromotionCommand = new RemoveCatalogPromotion('CATALOG_PROMOTION_CODE'); + + $commandBus->dispatch($updateCatalogPromotionStateCommand)->willReturn(new Envelope($updateCatalogPromotionStateCommand))->shouldBeCalled(); + $commandBus->dispatch($disableCatalogPromotionCommand)->willReturn(new Envelope($disableCatalogPromotionCommand))->shouldBeCalled(); + $commandBus->dispatch($removeCatalogPromotionCommand)->willReturn(new Envelope($removeCatalogPromotionCommand))->shouldBeCalled(); + + $this->dispatchCatalogPromotionRemoval($catalogPromotion); + } + + function it_dispatches_remove_catalog_promotion_command_on_disabled_catalog_promotion( + MessageBusInterface $commandBus, + CatalogPromotionInterface $catalogPromotion, + ): void { + $catalogPromotion->getCode()->willReturn('CATALOG_PROMOTION_CODE'); + $catalogPromotion->isEnabled()->willReturn(false); + + $updateCatalogPromotionStateCommand = new UpdateCatalogPromotionState('CATALOG_PROMOTION_CODE'); + $disableCatalogPromotionCommand = new DisableCatalogPromotion('CATALOG_PROMOTION_CODE'); + $removeCatalogPromotionCommand = new RemoveCatalogPromotion('CATALOG_PROMOTION_CODE'); + + $commandBus->dispatch($updateCatalogPromotionStateCommand)->willReturn(new Envelope($updateCatalogPromotionStateCommand))->shouldBeCalled(); + $commandBus->dispatch($disableCatalogPromotionCommand)->shouldNotBeCalled(); + $commandBus->dispatch($removeCatalogPromotionCommand)->willReturn(new Envelope($removeCatalogPromotionCommand))->shouldBeCalled(); + + $this->dispatchCatalogPromotionRemoval($catalogPromotion); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/CommandHandler/DisableCatalogPromotionHandlerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/CommandHandler/DisableCatalogPromotionHandlerSpec.php new file mode 100644 index 0000000000..8581e6ebbe --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/CommandHandler/DisableCatalogPromotionHandlerSpec.php @@ -0,0 +1,56 @@ +beConstructedWith($catalogPromotionRepository, $allProductVariantsCatalogPromotionsProcessor); + } + + public function it_disables_catalog_promotion( + CatalogPromotionRepositoryInterface $catalogPromotionRepository, + AllProductVariantsCatalogPromotionsProcessorInterface $allProductVariantsCatalogPromotionsProcessor, + CatalogPromotionInterface $catalogPromotion, + ): void { + $catalogPromotionRepository->findOneBy(['code' => 'CATALOG_PROMOTION_CODE'])->willReturn($catalogPromotion); + + $catalogPromotion->disable()->shouldBeCalled(); + $allProductVariantsCatalogPromotionsProcessor->process()->shouldBeCalled(); + + $this(new DisableCatalogPromotion('CATALOG_PROMOTION_CODE')); + } + + public function it_returns_if_there_is_no_catalog_promotion_with_given_code( + CatalogPromotionRepositoryInterface $catalogPromotionRepository, + AllProductVariantsCatalogPromotionsProcessorInterface $allProductVariantsCatalogPromotionsProcessor, + CatalogPromotionInterface $catalogPromotion, + ): void { + $catalogPromotionRepository->findOneBy(['code' => 'CATALOG_PROMOTION_CODE'])->willReturn(null); + + $catalogPromotion->disable()->shouldNotBeCalled(); + $allProductVariantsCatalogPromotionsProcessor->process()->shouldNotBeCalled(); + + $this(new DisableCatalogPromotion('CATALOG_PROMOTION_CODE')); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/CommandHandler/RemoveCatalogPromotionHandlerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/CommandHandler/RemoveCatalogPromotionHandlerSpec.php new file mode 100644 index 0000000000..1cc7018bf9 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/CommandHandler/RemoveCatalogPromotionHandlerSpec.php @@ -0,0 +1,76 @@ +beConstructedWith($catalogPromotionRepository, $entityManager); + } + + public function it_removes_catalog_promotion_being_processed( + CatalogPromotionRepositoryInterface $catalogPromotionRepository, + EntityManager $entityManager, + CatalogPromotionInterface $catalogPromotion, + ): void { + $catalogPromotionRepository->findOneBy(['code' => 'CATALOG_PROMOTION_CODE'])->willReturn($catalogPromotion); + + $catalogPromotion->getState()->willReturn(CatalogPromotionStates::STATE_PROCESSING); + + $this(new RemoveCatalogPromotion('CATALOG_PROMOTION_CODE')); + + $entityManager->remove($catalogPromotion)->shouldBeCalled(); + } + + public function it_throws_an_exception_if_catalog_promotion_is_not_in_a_processing_state( + CatalogPromotionRepositoryInterface $catalogPromotionRepository, + EntityManager $entityManager, + CatalogPromotionInterface $catalogPromotion, + ): void { + $catalogPromotionRepository->findOneBy(['code' => 'CATALOG_PROMOTION_CODE'])->willReturn($catalogPromotion); + + $catalogPromotion->getState()->willReturn(CatalogPromotionStates::STATE_ACTIVE); + $catalogPromotion->getCode()->willReturn('CATALOG_PROMOTION_CODE'); + + $entityManager->remove(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(InvalidCatalogPromotionStateException::class) + ->during('__invoke', [new RemoveCatalogPromotion('CATALOG_PROMOTION_CODE')]) + ; + } + + public function it_returns_if_there_is_no_catalog_promotion_with_given_code( + CatalogPromotionRepositoryInterface $catalogPromotionRepository, + EntityManager $entityManager, + ): void { + $catalogPromotionRepository->findOneBy(['code' => 'CATALOG_PROMOTION_CODE'])->willReturn(null); + + $entityManager->remove(Argument::any())->shouldNotBeCalled(); + + $this(new RemoveCatalogPromotion('CATALOG_PROMOTION_CODE')); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Processor/CatalogPromotionRemovalProcessorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Processor/CatalogPromotionRemovalProcessorSpec.php index 476ee3d9e2..9500335afe 100644 --- a/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Processor/CatalogPromotionRemovalProcessorSpec.php +++ b/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Processor/CatalogPromotionRemovalProcessorSpec.php @@ -15,85 +15,63 @@ namespace spec\Sylius\Bundle\CoreBundle\CatalogPromotion\Processor; use PhpSpec\ObjectBehavior; use Prophecy\Argument; -use Sylius\Bundle\CoreBundle\CatalogPromotion\Command\RemoveInactiveCatalogPromotion; +use Sylius\Bundle\CoreBundle\CatalogPromotion\Announcer\CatalogPromotionRemovalAnnouncerInterface; +use Sylius\Bundle\CoreBundle\CatalogPromotion\Processor\CatalogPromotionRemovalProcessorInterface; use Sylius\Component\Core\Model\CatalogPromotionInterface; -use Sylius\Component\Promotion\Event\CatalogPromotionEnded; -use Sylius\Component\Promotion\Event\CatalogPromotionUpdated; use Sylius\Component\Promotion\Exception\CatalogPromotionNotFoundException; use Sylius\Component\Promotion\Exception\InvalidCatalogPromotionStateException; +use Sylius\Component\Promotion\Model\CatalogPromotionStates; use Sylius\Component\Promotion\Repository\CatalogPromotionRepositoryInterface; -use Symfony\Component\Messenger\Envelope; -use Symfony\Component\Messenger\MessageBusInterface; final class CatalogPromotionRemovalProcessorSpec extends ObjectBehavior { public function let( CatalogPromotionRepositoryInterface $catalogPromotionRepository, - MessageBusInterface $commandBus, - MessageBusInterface $eventBus, + CatalogPromotionRemovalAnnouncerInterface $catalogPromotionRemovalAnnouncer, ): void { - $this->beConstructedWith($catalogPromotionRepository, $commandBus, $eventBus); + $this->beConstructedWith($catalogPromotionRepository, $catalogPromotionRemovalAnnouncer); } - public function it_removes_an_active_catalog_promotion_by_disabling_it_and_dispatching_catalog_promotion_ended_event_and_remove_inactive_catalog_promotion_command( + public function it_implements_catalog_promotion_removal_processor_interface(): void + { + $this->shouldImplement(CatalogPromotionRemovalProcessorInterface::class); + } + + public function it_removes_an_active_catalog_promotion( CatalogPromotionRepositoryInterface $catalogPromotionRepository, - MessageBusInterface $commandBus, - MessageBusInterface $eventBus, + CatalogPromotionRemovalAnnouncerInterface $catalogPromotionRemovalAnnouncer, CatalogPromotionInterface $catalogPromotion, ): void { $catalogPromotionRepository->findOneBy(['code' => 'CATALOG_PROMOTION_CODE'])->willReturn($catalogPromotion); + $catalogPromotion->getState()->willReturn(CatalogPromotionStates::STATE_ACTIVE); - $catalogPromotion->getState()->willReturn('active'); - - $catalogPromotion->setEnabled(false)->shouldBeCalled(); - - $event = new CatalogPromotionEnded('CATALOG_PROMOTION_CODE'); - $command = new RemoveInactiveCatalogPromotion('CATALOG_PROMOTION_CODE'); - - $eventBus->dispatch($event)->willReturn(new Envelope($event)); - $commandBus->dispatch($command)->willReturn(new Envelope($command)); + $catalogPromotionRemovalAnnouncer->dispatchCatalogPromotionRemoval($catalogPromotion)->shouldBeCalled(); $this->removeCatalogPromotion('CATALOG_PROMOTION_CODE'); } - public function it_removes_an_inactive_catalog_promotion_by_dispatching_remove_inactive_catalog_promotion_command_without_recalculating_the_catalog( + public function it_removes_an_inactive_catalog_promotion( CatalogPromotionRepositoryInterface $catalogPromotionRepository, - MessageBusInterface $commandBus, - MessageBusInterface $eventBus, + CatalogPromotionRemovalAnnouncerInterface $catalogPromotionRemovalAnnouncer, CatalogPromotionInterface $catalogPromotion, ): void { $catalogPromotionRepository->findOneBy(['code' => 'CATALOG_PROMOTION_CODE'])->willReturn($catalogPromotion); + $catalogPromotion->getState()->willReturn(CatalogPromotionStates::STATE_INACTIVE); - $catalogPromotion->getState()->willReturn('inactive'); - - $catalogPromotion->setEnabled(Argument::any())->shouldNotBeCalled(); - - $event = new CatalogPromotionEnded('CATALOG_PROMOTION_CODE'); - $command = new RemoveInactiveCatalogPromotion('CATALOG_PROMOTION_CODE'); - - $eventBus->dispatch($event)->shouldNotBeCalled(); - $commandBus->dispatch($command)->willReturn(new Envelope($command)); + $catalogPromotionRemovalAnnouncer->dispatchCatalogPromotionRemoval($catalogPromotion)->shouldBeCalled(); $this->removeCatalogPromotion('CATALOG_PROMOTION_CODE'); } - public function it_does_not_dispatch_any_events_and_commands_if_catalog_promotion_from_command_does_not_exist( + public function it_does_not_dispatch_catalog_promotion_removal_if_catalog_promotion_from_command_does_not_exist( CatalogPromotionRepositoryInterface $catalogPromotionRepository, - MessageBusInterface $commandBus, - MessageBusInterface $eventBus, + CatalogPromotionRemovalAnnouncerInterface $catalogPromotionRemovalAnnouncer, CatalogPromotionInterface $catalogPromotion, ): void { $catalogPromotionRepository->findOneBy(['code' => 'CATALOG_PROMOTION_CODE'])->willReturn(null); - $catalogPromotion->getState()->shouldNotBeCalled(); - $catalogPromotion->setEnabled(Argument::any())->shouldNotBeCalled(); - - $event = new CatalogPromotionUpdated('CATALOG_PROMOTION_CODE'); - $command = new RemoveInactiveCatalogPromotion('CATALOG_PROMOTION_CODE'); - - $eventBus->dispatch($event)->shouldNotBeCalled(); - $commandBus->dispatch($command)->shouldNotBeCalled(); + $catalogPromotionRemovalAnnouncer->dispatchCatalogPromotionRemoval(Argument::any())->shouldNotBeCalled(); $this ->shouldThrow(CatalogPromotionNotFoundException::class) @@ -103,21 +81,13 @@ final class CatalogPromotionRemovalProcessorSpec extends ObjectBehavior public function it_throws_an_exception_if_catalog_promotion_is_being_processed( CatalogPromotionRepositoryInterface $catalogPromotionRepository, - MessageBusInterface $commandBus, - MessageBusInterface $eventBus, + CatalogPromotionRemovalAnnouncerInterface $catalogPromotionRemovalAnnouncer, CatalogPromotionInterface $catalogPromotion, ): void { $catalogPromotionRepository->findOneBy(['code' => 'CATALOG_PROMOTION_CODE'])->willReturn($catalogPromotion); + $catalogPromotion->getState()->willReturn(CatalogPromotionStates::STATE_PROCESSING); - $catalogPromotion->getState()->willReturn('processing'); - - $catalogPromotion->setEnabled(Argument::any())->shouldNotBeCalled(); - - $event = new CatalogPromotionEnded('CATALOG_PROMOTION_CODE'); - $command = new RemoveInactiveCatalogPromotion('CATALOG_PROMOTION_CODE'); - - $eventBus->dispatch($event)->shouldNotBeCalled(); - $commandBus->dispatch($command)->shouldNotBeCalled(); + $catalogPromotionRemovalAnnouncer->dispatchCatalogPromotionRemoval(Argument::any())->shouldNotBeCalled(); $this ->shouldThrow(InvalidCatalogPromotionStateException::class) @@ -127,21 +97,13 @@ final class CatalogPromotionRemovalProcessorSpec extends ObjectBehavior public function it_throws_an_exception_if_catalog_promotion_state_is_out_of_invalid_one( CatalogPromotionRepositoryInterface $catalogPromotionRepository, - MessageBusInterface $commandBus, - MessageBusInterface $eventBus, + CatalogPromotionRemovalAnnouncerInterface $catalogPromotionRemovalAnnouncer, CatalogPromotionInterface $catalogPromotion, ): void { $catalogPromotionRepository->findOneBy(['code' => 'CATALOG_PROMOTION_CODE'])->willReturn($catalogPromotion); - $catalogPromotion->getState()->willReturn('invalid_state'); - $catalogPromotion->setEnabled(Argument::any())->shouldNotBeCalled(); - - $event = new CatalogPromotionEnded('CATALOG_PROMOTION_CODE'); - $command = new RemoveInactiveCatalogPromotion('CATALOG_PROMOTION_CODE'); - - $eventBus->dispatch($event)->shouldNotBeCalled(); - $commandBus->dispatch($command)->shouldNotBeCalled(); + $catalogPromotionRemovalAnnouncer->dispatchCatalogPromotionRemoval(Argument::any())->shouldNotBeCalled(); $this ->shouldThrow(\DomainException::class) diff --git a/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Processor/CatalogPromotionStateProcessorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Processor/CatalogPromotionStateProcessorSpec.php index 60e33c4d35..a6f593c85f 100644 --- a/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Processor/CatalogPromotionStateProcessorSpec.php +++ b/src/Sylius/Bundle/CoreBundle/spec/CatalogPromotion/Processor/CatalogPromotionStateProcessorSpec.php @@ -15,7 +15,8 @@ namespace spec\Sylius\Bundle\CoreBundle\CatalogPromotion\Processor; use PhpSpec\ObjectBehavior; use SM\Factory\FactoryInterface; -use SM\StateMachine\StateMachineInterface; +use SM\StateMachine\StateMachineInterface as WinzouStateMachineInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\CoreBundle\CatalogPromotion\Checker\CatalogPromotionEligibilityCheckerInterface; use Sylius\Bundle\CoreBundle\CatalogPromotion\Processor\CatalogPromotionStateProcessorInterface; use Sylius\Component\Core\Model\CatalogPromotionInterface; @@ -39,7 +40,7 @@ final class CatalogPromotionStateProcessorSpec extends ObjectBehavior CatalogPromotionEligibilityCheckerInterface $catalogPromotionEligibilityChecker, CatalogPromotionInterface $catalogPromotion, FactoryInterface $stateMachineFactory, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $stateMachineFactory->get($catalogPromotion, CatalogPromotionTransitions::GRAPH)->willReturn($stateMachine); @@ -54,11 +55,29 @@ final class CatalogPromotionStateProcessorSpec extends ObjectBehavior $this->process($catalogPromotion); } + function it_uses_the_new_state_machine_adapter_if_passed( + CatalogPromotionEligibilityCheckerInterface $catalogPromotionEligibilityChecker, + CatalogPromotionInterface $catalogPromotion, + StateMachineInterface $stateMachine, + ): void { + $this->beConstructedWith($catalogPromotionEligibilityChecker, $stateMachine); + + $catalogPromotionEligibilityChecker->isCatalogPromotionEligible($catalogPromotion)->willReturn(true); + + $stateMachine->can($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_PROCESS)->willReturn(true); + $stateMachine->apply($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_PROCESS)->shouldBeCalled(); + + $stateMachine->apply($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_ACTIVATE)->shouldNotBeCalled(); + $stateMachine->apply($catalogPromotion, CatalogPromotionTransitions::GRAPH, CatalogPromotionTransitions::TRANSITION_DEACTIVATE)->shouldNotBeCalled(); + + $this->process($catalogPromotion); + } + function it_activates_a_catalog_promotion( CatalogPromotionEligibilityCheckerInterface $catalogPromotionEligibilityChecker, CatalogPromotionInterface $catalogPromotion, FactoryInterface $stateMachineFactory, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $stateMachineFactory->get($catalogPromotion, CatalogPromotionTransitions::GRAPH)->willReturn($stateMachine); @@ -78,7 +97,7 @@ final class CatalogPromotionStateProcessorSpec extends ObjectBehavior CatalogPromotionEligibilityCheckerInterface $catalogPromotionEligibilityChecker, CatalogPromotionInterface $catalogPromotion, FactoryInterface $stateMachineFactory, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $stateMachineFactory->get($catalogPromotion, CatalogPromotionTransitions::GRAPH)->willReturn($stateMachine); diff --git a/src/Sylius/Bundle/CoreBundle/spec/Checkout/CheckoutResolverSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Checkout/CheckoutResolverSpec.php index 4a6a41ee38..b30d7a2790 100644 --- a/src/Sylius/Bundle/CoreBundle/spec/Checkout/CheckoutResolverSpec.php +++ b/src/Sylius/Bundle/CoreBundle/spec/Checkout/CheckoutResolverSpec.php @@ -16,7 +16,8 @@ namespace spec\Sylius\Bundle\CoreBundle\Checkout; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use SM\Factory\FactoryInterface; -use SM\StateMachine\StateMachineInterface; +use SM\StateMachine\StateMachineInterface as WinzouStateMachineInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\CoreBundle\Checkout\CheckoutStateUrlGeneratorInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Order\Context\CartContextInterface; @@ -160,7 +161,7 @@ final class CheckoutResolverSpec extends ObjectBehavior CartContextInterface $cartContext, OrderInterface $order, FactoryInterface $stateMachineFactory, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, ): void { $request = new Request([], [], [ '_sylius' => ['state_machine' => ['graph' => 'test_graph', 'transition' => 'test_transition']], @@ -183,7 +184,7 @@ final class CheckoutResolverSpec extends ObjectBehavior CartContextInterface $cartContext, OrderInterface $order, FactoryInterface $stateMachineFactory, - StateMachineInterface $stateMachine, + WinzouStateMachineInterface $stateMachine, CheckoutStateUrlGeneratorInterface $urlGenerator, ): void { $request = new Request([], [], [ @@ -201,4 +202,29 @@ final class CheckoutResolverSpec extends ObjectBehavior $this->onKernelRequest($event); } + + function it_uses_the_new_state_machine_abstraction_if_passed( + CartContextInterface $cartContext, + CheckoutStateUrlGeneratorInterface $urlGenerator, + RequestMatcherInterface $requestMatcher, + StateMachineInterface $stateMachine, + RequestEvent $event, + OrderInterface $order, + ): void { + $this->beConstructedWith($cartContext, $urlGenerator, $requestMatcher, $stateMachine); + + $request = new Request([], [], [ + '_sylius' => ['state_machine' => ['graph' => 'test_graph', 'transition' => 'test_transition']], + ]); + $event->isMainRequest()->willReturn(true); + $event->getRequest()->willReturn($request); + $requestMatcher->matches($request)->willReturn(true); + $cartContext->getCart()->willReturn($order); + $order->isEmpty()->willReturn(false); + $stateMachine->can($order, 'test_graph', 'test_transition')->willReturn(false); + $urlGenerator->generateForOrderCheckoutState($order)->willReturn('/target-url'); + $event->setResponse(Argument::type(RedirectResponse::class))->shouldBeCalled(); + + $this->onKernelRequest($event); + } } diff --git a/src/Sylius/Bundle/CoreBundle/spec/Command/Model/PluginInfoSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Console/Command/Model/PluginInfoSpec.php similarity index 94% rename from src/Sylius/Bundle/CoreBundle/spec/Command/Model/PluginInfoSpec.php rename to src/Sylius/Bundle/CoreBundle/spec/Console/Command/Model/PluginInfoSpec.php index 62b066a820..5f0d9c3914 100644 --- a/src/Sylius/Bundle/CoreBundle/spec/Command/Model/PluginInfoSpec.php +++ b/src/Sylius/Bundle/CoreBundle/spec/Console/Command/Model/PluginInfoSpec.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace spec\Sylius\Bundle\CoreBundle\Command\Model; +namespace spec\Sylius\Bundle\CoreBundle\Console\Command\Model; use PhpSpec\ObjectBehavior; diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/MailerListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/MailerListenerSpec.php index 858162d29f..6a86e485a4 100644 --- a/src/Sylius/Bundle/CoreBundle/spec/EventListener/MailerListenerSpec.php +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/MailerListenerSpec.php @@ -17,7 +17,7 @@ use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Sylius\Bundle\CoreBundle\Mailer\Emails; use Sylius\Component\Channel\Context\ChannelContextInterface; -use Sylius\Component\Channel\Model\ChannelInterface; +use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\CustomerInterface; use Sylius\Component\Core\Model\ShopUserInterface; use Sylius\Component\Locale\Context\LocaleContextInterface; @@ -42,9 +42,12 @@ final class MailerListenerSpec extends ObjectBehavior function it_throws_an_exception_if_event_subject_is_not_a_customer_instance_sending_confirmation( GenericEvent $event, \stdClass $customer, + ChannelInterface $channel, ): void { $event->getSubject()->willReturn($customer); + $channel->isAccountVerificationRequired()->willReturn(false); + $this->shouldThrow(\InvalidArgumentException::class)->during('sendUserRegistrationEmail', [$event]); } @@ -52,10 +55,13 @@ final class MailerListenerSpec extends ObjectBehavior SenderInterface $emailSender, GenericEvent $event, CustomerInterface $customer, + ChannelInterface $channel, ): void { $event->getSubject()->willReturn($customer); $customer->getUser()->willReturn(null); + $channel->isAccountVerificationRequired()->willReturn(false); + $emailSender->send(Argument::cetera())->shouldNotBeCalled(); $this->sendUserRegistrationEmail($event); @@ -66,11 +72,14 @@ final class MailerListenerSpec extends ObjectBehavior GenericEvent $event, CustomerInterface $customer, ShopUserInterface $user, + ChannelInterface $channel, ): void { $event->getSubject()->willReturn($customer); $customer->getUser()->willReturn($user); $customer->getEmail()->willReturn(null); + $channel->isAccountVerificationRequired()->willReturn(false); + $emailSender->send(Argument::cetera())->shouldNotBeCalled(); $this->sendUserRegistrationEmail($event); @@ -89,6 +98,8 @@ final class MailerListenerSpec extends ObjectBehavior $user->getEmail()->willReturn('fulanito@sylius.com'); + $channel->isAccountVerificationRequired()->willReturn(false); + $emailSender->send(Emails::USER_REGISTRATION, ['fulanito@sylius.com'], [ 'user' => $user, 'channel' => $channel, @@ -98,6 +109,17 @@ final class MailerListenerSpec extends ObjectBehavior $this->sendUserRegistrationEmail($event); } + function it_does_nothing_when_account_verification_is_required( + ChannelInterface $channel, + GenericEvent $event, + ): void { + $channel->isAccountVerificationRequired()->willReturn(true); + + $event->getSubject()->shouldNotBeCalled(); + + $this->sendUserRegistrationEmail($event); + } + function it_send_password_reset_token_mail( SenderInterface $emailSender, ChannelInterface $channel, @@ -135,4 +157,34 @@ final class MailerListenerSpec extends ObjectBehavior $this->sendResetPasswordPinEmail($event); } + + function it_sends_verification_success_email( + SenderInterface $emailSender, + GenericEvent $event, + ShopUserInterface $shopUser, + ChannelInterface $channel, + ): void { + $event->getSubject()->willReturn($shopUser); + $shopUser->getEmail()->willReturn('shop@example.com'); + + $emailSender->send( + Emails::USER_REGISTRATION, + ['shop@example.com'], + [ + 'user' => $shopUser, + 'channel' => $channel, + 'localeCode' => 'en_US', + ], + )->shouldBeCalled(); + + $this->sendVerificationSuccessEmail($event); + } + + function it_throws_exception_while_sending_verification_success_email_when_event_subject_is_not_a_shop_user( + GenericEvent $event, + ): void { + $event->getSubject()->willReturn(new \stdClass()); + + $this->shouldThrow(\InvalidArgumentException::class)->during('sendVerificationSuccessEmail', [$event]); + } } diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/PostgreSQLDefaultSchemaListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/PostgreSQLDefaultSchemaListenerSpec.php new file mode 100644 index 0000000000..dcacff67e5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/PostgreSQLDefaultSchemaListenerSpec.php @@ -0,0 +1,64 @@ +createSchemaManager()->willReturn($schemaManager); + $entityManager->getConnection()->willReturn($connection); + $args->getEntityManager()->willReturn($entityManager); + + $args->getSchema()->shouldNotBeCalled(); + $schemaManager->listSchemaNames()->shouldNotBeCalled(); + + $this->postGenerateSchema($args); + } + + function it_creates_namespaces_for_all_schemas_in_the_current_database_if_schema_manager_is_postgresql( + Connection $connection, + EntityManagerInterface $entityManager, + GenerateSchemaEventArgs $args, + PostgreSQLSchemaManager $schemaManager, + Schema $schema, + ): void { + $connection->createSchemaManager()->willReturn($schemaManager); + $entityManager->getConnection()->willReturn($connection); + $args->getEntityManager()->willReturn($entityManager); + + $schemaManager->listSchemaNames()->willReturn(['public', 'information_schema']); + + $args->getSchema()->willReturn($schema); + + $schema->hasNamespace('public')->willReturn(false); + $schema->hasNamespace('information_schema')->willReturn(false); + $schema->createNamespace('public')->shouldBeCalled(); + $schema->createNamespace('information_schema')->shouldBeCalled(); + + $this->postGenerateSchema($args); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderNumberListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderNumberListenerSpec.php new file mode 100644 index 0000000000..4ba629d86d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderNumberListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderNumberAssigner); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $subject): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new TransitionEvent($subject->getWrappedObject(), new Marking())]) + ; + } + + function it_assigns_order_number(OrderNumberAssignerInterface $orderNumberAssigner): void + { + $order = new Order(); + $event = new TransitionEvent($order, new Marking()); + + $orderNumberAssigner->assignNumber($order)->shouldBeCalled(); + + $this($event); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderTokenListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderTokenListenerSpec.php new file mode 100644 index 0000000000..2877134759 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/AssignOrderTokenListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderTokenAssigner); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $subject): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new TransitionEvent($subject->getWrappedObject(), new Marking())]) + ; + } + + function it_assigns_order_token(OrderTokenAssignerInterface $orderTokenAssigner): void + { + $order = new Order(); + $event = new TransitionEvent($order, new Marking()); + + $orderTokenAssigner->assignTokenValueIfNotSet($order)->shouldBeCalled(); + + $this($event); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderPaymentListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderPaymentListenerSpec.php new file mode 100644 index 0000000000..2932e64c82 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderPaymentListenerSpec.php @@ -0,0 +1,74 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_does_nothing_if_order_cannot_have_payment_cancelled( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $compositeStateMachine + ->can($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_CANCEL) + ->willReturn(false) + ; + + $this($event); + + $compositeStateMachine + ->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_CANCEL) + ->shouldNotHaveBeenCalled() + ; + } + + function it_applies_transition_cancel_on_order_payment( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $compositeStateMachine + ->can($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_CANCEL) + ->willReturn(true) + ; + + $this($event); + + $compositeStateMachine + ->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_CANCEL) + ->shouldBeCalled() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderShippingListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderShippingListenerSpec.php new file mode 100644 index 0000000000..422512e7d9 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelOrderShippingListenerSpec.php @@ -0,0 +1,74 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_does_nothing_if_order_cannot_have_shipping_cancelled( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $compositeStateMachine + ->can($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_CANCEL) + ->willReturn(false) + ; + + $this($event); + + $compositeStateMachine + ->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_CANCEL) + ->shouldNotHaveBeenCalled() + ; + } + + function it_applies_transition_cancel_on_order_shipping( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $compositeStateMachine + ->can($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_CANCEL) + ->willReturn(true) + ; + + $this($event); + + $compositeStateMachine + ->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_CANCEL) + ->shouldBeCalled() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelPaymentListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelPaymentListenerSpec.php new file mode 100644 index 0000000000..fd67ea9039 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelPaymentListenerSpec.php @@ -0,0 +1,90 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_does_nothing_if_payment_cannot_be_cancelled( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + PaymentInterface $payment, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + $order->getPayments()->willReturn(new ArrayCollection([$payment->getWrappedObject()])); + $compositeStateMachine + ->can($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL) + ->willReturn(false) + ; + + $this($event); + + $compositeStateMachine + ->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL) + ->shouldNotHaveBeenCalled() + ; + } + + function it_applies_transition_cancel_on_payments( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + PaymentInterface $payment1, + PaymentInterface $payment2, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + $order->getPayments()->willReturn(new ArrayCollection([$payment1->getWrappedObject(), $payment2->getWrappedObject()])); + + $compositeStateMachine + ->can($payment1, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL) + ->willReturn(true) + ; + + $compositeStateMachine + ->can($payment2, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL) + ->willReturn(true) + ; + + $this($event); + + $compositeStateMachine + ->apply($payment1, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL) + ->shouldHaveBeenCalledOnce() + ; + + $compositeStateMachine + ->apply($payment2, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL) + ->shouldHaveBeenCalledOnce() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelShipmentListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelShipmentListenerSpec.php new file mode 100644 index 0000000000..b7b1be7aa7 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CancelShipmentListenerSpec.php @@ -0,0 +1,90 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_does_nothing_if_shipment_cannot_be_cancelled( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ShipmentInterface $shipment, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + $order->getShipments()->willReturn(new ArrayCollection([$shipment->getWrappedObject()])); + $compositeStateMachine + ->can($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL) + ->willReturn(false); + + $this($event); + + $compositeStateMachine + ->apply($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL) + ->shouldNotHaveBeenCalled() + ; + } + + function it_applies_transition_cancel_on_shipments( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ShipmentInterface $shipment1, + ShipmentInterface $shipment2, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + $order->getShipments()->willReturn(new ArrayCollection([$shipment1->getWrappedObject(), $shipment2->getWrappedObject()])); + + $compositeStateMachine + ->can($shipment1, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL) + ->willReturn(true) + ; + + $compositeStateMachine + ->can($shipment2, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL) + ->willReturn(true) + ; + + $this($event); + + $compositeStateMachine + ->apply($shipment1, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL) + ->shouldHaveBeenCalledOnce() + ; + + $compositeStateMachine + ->apply($shipment2, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CANCEL) + ->shouldHaveBeenCalledOnce() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CreatePaymentListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CreatePaymentListenerSpec.php new file mode 100644 index 0000000000..11a94e3ab8 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CreatePaymentListenerSpec.php @@ -0,0 +1,90 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_does_nothing_if_payment_cannot_be_created( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + PaymentInterface $payment, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + $order->getPayments()->willReturn(new ArrayCollection([$payment->getWrappedObject()])); + $compositeStateMachine + ->can($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CREATE) + ->willReturn(false) + ; + + $this($event); + + $compositeStateMachine + ->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CREATE) + ->shouldNotHaveBeenCalled() + ; + } + + function it_applies_transition_create_on_payments( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + PaymentInterface $payment1, + PaymentInterface $payment2, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + $order->getPayments()->willReturn(new ArrayCollection([$payment1->getWrappedObject(), $payment2->getWrappedObject()])); + + $compositeStateMachine + ->can($payment1, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CREATE) + ->willReturn(true) + ; + + $compositeStateMachine + ->can($payment2, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CREATE) + ->willReturn(true) + ; + + $this($event); + + $compositeStateMachine + ->apply($payment1, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CREATE) + ->shouldHaveBeenCalledOnce() + ; + + $compositeStateMachine + ->apply($payment2, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CREATE) + ->shouldHaveBeenCalledOnce() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CreateShipmentListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CreateShipmentListenerSpec.php new file mode 100644 index 0000000000..23a017d46f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/CreateShipmentListenerSpec.php @@ -0,0 +1,90 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_does_nothing_if_shipment_cannot_be_created( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ShipmentInterface $shipment, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + $order->getShipments()->willReturn(new ArrayCollection([$shipment->getWrappedObject()])); + $compositeStateMachine + ->can($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CREATE) + ->willReturn(false); + + $this($event); + + $compositeStateMachine + ->apply($shipment, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CREATE) + ->shouldNotHaveBeenCalled() + ; + } + + function it_applies_transition_create_on_shipments( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ShipmentInterface $shipment1, + ShipmentInterface $shipment2, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + $order->getShipments()->willReturn(new ArrayCollection([$shipment1->getWrappedObject(), $shipment2->getWrappedObject()])); + + $compositeStateMachine + ->can($shipment1, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CREATE) + ->willReturn(true) + ; + + $compositeStateMachine + ->can($shipment2, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CREATE) + ->willReturn(true) + ; + + $this($event); + + $compositeStateMachine + ->apply($shipment1, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CREATE) + ->shouldHaveBeenCalledOnce() + ; + + $compositeStateMachine + ->apply($shipment2, ShipmentTransitions::GRAPH, ShipmentTransitions::TRANSITION_CREATE) + ->shouldHaveBeenCalledOnce() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/DecrementPromotionUsagesListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/DecrementPromotionUsagesListenerSpec.php new file mode 100644 index 0000000000..362baf14db --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/DecrementPromotionUsagesListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderPromotionsUsageModifier); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_decrements_promotion_usages( + OrderPromotionsUsageModifierInterface $orderPromotionsUsageModifier, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderPromotionsUsageModifier->decrement($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/GiveBackInventoryListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/GiveBackInventoryListenerSpec.php new file mode 100644 index 0000000000..fac019f2f5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/GiveBackInventoryListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderInventoryOperator); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_cancels_order( + OrderInventoryOperatorInterface $orderInventoryOperator, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderInventoryOperator->cancel($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/HoldInventoryListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/HoldInventoryListenerSpec.php new file mode 100644 index 0000000000..51cec5be30 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/HoldInventoryListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderInventoryOperator); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_holds_order_inventory( + OrderInventoryOperatorInterface $orderInventoryOperator, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderInventoryOperator->hold($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/IncrementPromotionUsagesListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/IncrementPromotionUsagesListenerSpec.php new file mode 100644 index 0000000000..dccd821bd8 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/IncrementPromotionUsagesListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderPromotionsUsageModifier); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_increments_promotion_usages( + OrderPromotionsUsageModifierInterface $orderPromotionsUsageModifier, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderPromotionsUsageModifier->increment($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderPaymentListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderPaymentListenerSpec.php new file mode 100644 index 0000000000..8ea42dba29 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderPaymentListenerSpec.php @@ -0,0 +1,75 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_does_nothing_if_order_cannot_have_payment_requested( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $compositeStateMachine + ->can($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_REQUEST_PAYMENT) + ->willReturn(false) + ; + + $this($event); + + $compositeStateMachine + ->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_REQUEST_PAYMENT) + ->shouldNotHaveBeenCalled() + ; + } + + function it_applies_transition_request_payment_on_order_payment( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $compositeStateMachine + ->can($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_REQUEST_PAYMENT) + ->willReturn(true) + ; + + $this($event); + + $compositeStateMachine + ->apply($order, OrderPaymentTransitions::GRAPH, OrderPaymentTransitions::TRANSITION_REQUEST_PAYMENT) + ->shouldBeCalled() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderShippingListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderShippingListenerSpec.php new file mode 100644 index 0000000000..ea8b79b9ae --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/RequestOrderShippingListenerSpec.php @@ -0,0 +1,75 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_does_nothing_if_order_cannot_have_shipping_requested( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $compositeStateMachine + ->can($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_REQUEST_SHIPPING) + ->willReturn(false) + ; + + $this($event); + + $compositeStateMachine + ->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_REQUEST_SHIPPING) + ->shouldNotHaveBeenCalled() + ; + } + + function it_applies_transition_request_shipping_on_order_shipping( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $compositeStateMachine + ->can($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_REQUEST_SHIPPING) + ->willReturn(true) + ; + + $this($event); + + $compositeStateMachine + ->apply($order, OrderShippingTransitions::GRAPH, OrderShippingTransitions::TRANSITION_REQUEST_SHIPPING) + ->shouldBeCalled() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SaveCustomerAddressesListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SaveCustomerAddressesListenerSpec.php new file mode 100644 index 0000000000..6ddb5976f4 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SaveCustomerAddressesListenerSpec.php @@ -0,0 +1,47 @@ +beConstructedWith($orderAddressesSaver); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_saves_addresses( + OrderAddressesSaverInterface $orderAddressesSaver, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderAddressesSaver->saveAddresses($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SetImmutableNamesListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SetImmutableNamesListenerSpec.php new file mode 100644 index 0000000000..1b5631986c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Order/SetImmutableNamesListenerSpec.php @@ -0,0 +1,47 @@ +beConstructedWith($orderItemNamesSetter); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_sets_immutable_names( + OrderItemNamesSetterInterface $orderItemNamesSetter, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderItemNamesSetter->__invoke($order)->shouldBeCalled(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ApplyCreateTransitionOnOrderListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ApplyCreateTransitionOnOrderListenerSpec.php new file mode 100644 index 0000000000..abba4e8741 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ApplyCreateTransitionOnOrderListenerSpec.php @@ -0,0 +1,74 @@ +beConstructedWith($compositeStateMachine); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_does_nothing_if_order_cannot_be_created( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $compositeStateMachine + ->can($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CREATE) + ->willReturn(false) + ; + + $this($event); + + $compositeStateMachine + ->apply($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CREATE) + ->shouldNotHaveBeenCalled() + ; + } + + function it_applies_transition_create_on_order( + StateMachineInterface $compositeStateMachine, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $compositeStateMachine + ->can($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CREATE) + ->willReturn(true) + ; + + $this($event); + + $compositeStateMachine + ->apply($order, OrderTransitions::GRAPH, OrderTransitions::TRANSITION_CREATE) + ->shouldHaveBeenCalledOnce() + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ProcessCartListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ProcessCartListenerSpec.php new file mode 100644 index 0000000000..e4eeaa8fb2 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ProcessCartListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderProcessor); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_processes_order( + OrderProcessorInterface $orderProcessor, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderProcessor->process($order)->shouldHaveBeenCalledOnce(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ResolveOrderCheckoutStateListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ResolveOrderCheckoutStateListenerSpec.php new file mode 100644 index 0000000000..10f74e1849 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ResolveOrderCheckoutStateListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderCheckoutStateResolver); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_resolves_order_checkout_state_after_select_shipping( + StateResolverInterface $orderCheckoutStateResolver, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderCheckoutStateResolver->resolve($order)->shouldHaveBeenCalledOnce(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ResolveOrderPaymentStateListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ResolveOrderPaymentStateListenerSpec.php new file mode 100644 index 0000000000..412a96ca38 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ResolveOrderPaymentStateListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderPaymentStateResolver); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_resolves_order_payment_state_after_compete( + StateResolverInterface $orderPaymentStateResolver, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderPaymentStateResolver->resolve($order)->shouldHaveBeenCalledOnce(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ResolveOrderShippingStateListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ResolveOrderShippingStateListenerSpec.php new file mode 100644 index 0000000000..173a8fbb8c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/ResolveOrderShippingStateListenerSpec.php @@ -0,0 +1,46 @@ +beConstructedWith($orderShippingStateResolver); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_resolves_order_shipping_state_after_compete( + StateResolverInterface $orderShippingStateResolver, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderShippingStateResolver->resolve($order)->shouldHaveBeenCalledOnce(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/SaveCheckoutCompletionDateListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/SaveCheckoutCompletionDateListenerSpec.php new file mode 100644 index 0000000000..ad0512a5e6 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderCheckout/SaveCheckoutCompletionDateListenerSpec.php @@ -0,0 +1,39 @@ +shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]); + } + + function it_completes_checkout( + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $order->completeCheckout()->shouldHaveBeenCalledOnce(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderPayment/ResolveOrderStateListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderPayment/ResolveOrderStateListenerSpec.php new file mode 100644 index 0000000000..0ae62c3720 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderPayment/ResolveOrderStateListenerSpec.php @@ -0,0 +1,48 @@ +beConstructedWith($orderStateResolver); + } + + function it_throws_an_exception_on_non_supported_subject(stdClass $subject): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($subject->getWrappedObject(), new Marking())]) + ; + } + + function it_resolves_order_state( + StateResolverInterface $orderStateResolver, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderStateResolver->resolve($order)->shouldHaveBeenCalledOnce(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderPayment/SellOrderInventoryListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderPayment/SellOrderInventoryListenerSpec.php new file mode 100644 index 0000000000..39e3f8436e --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderPayment/SellOrderInventoryListenerSpec.php @@ -0,0 +1,48 @@ +beConstructedWith($orderInventoryOperator); + } + + function it_throws_an_exception_on_non_supported_subject(stdClass $subject): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($subject->getWrappedObject(), new Marking())]) + ; + } + + function it_sells_order_inventory( + OrderInventoryOperatorInterface $orderInventoryOperator, + OrderInterface $order, + ): void { + $event = new CompletedEvent($order->getWrappedObject(), new Marking()); + + $this($event); + + $orderInventoryOperator->sell($order)->shouldHaveBeenCalledOnce(); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderShipping/ResolveOrderStateListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderShipping/ResolveOrderStateListenerSpec.php new file mode 100644 index 0000000000..a82b4e277f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/OrderShipping/ResolveOrderStateListenerSpec.php @@ -0,0 +1,47 @@ +beConstructedWith($stateResolver); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $callback): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($callback->getWrappedObject(), new Marking())]) + ; + } + + function it_resolves_order_state_after_order_being_shipped( + StateResolverInterface $stateResolver, + ): void { + $order = new Order(); + $event = new CompletedEvent($order, new Marking()); + + $stateResolver->resolve($order)->shouldBeCalled(); + + $this($event); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Payment/ProcessOrderListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Payment/ProcessOrderListenerSpec.php new file mode 100644 index 0000000000..6f77f5fdbd --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Payment/ProcessOrderListenerSpec.php @@ -0,0 +1,59 @@ +beConstructedWith($orderProcessor); + } + + function it_throws_exception_when_event_subject_is_not_a_payment(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent(new \stdClass(), new Marking())]) + ; + } + + function it_throws_exception_when_event_payment_has_no_order(PaymentInterface $payment): void + { + $payment->getOrder()->willReturn(null); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($payment->getWrappedObject(), new Marking())]) + ; + } + + function it_processes_order( + OrderProcessorInterface $orderProcessor, + PaymentInterface $payment, + OrderInterface $order, + ): void { + $payment->getOrder()->willReturn($order); + + $orderProcessor->process($order)->shouldBeCalled(); + + $this->__invoke(new CompletedEvent($payment->getWrappedObject(), new Marking())); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Payment/ResolveOrderPaymentStateListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Payment/ResolveOrderPaymentStateListenerSpec.php new file mode 100644 index 0000000000..fd10aaa9b7 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Payment/ResolveOrderPaymentStateListenerSpec.php @@ -0,0 +1,59 @@ +beConstructedWith($stateResolver); + } + + function it_throws_exception_when_event_subject_is_not_a_payment(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent(new \stdClass(), new Marking())]) + ; + } + + function it_throws_exception_when_event_payment_has_no_order(PaymentInterface $payment): void + { + $payment->getOrder()->willReturn(null); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($payment->getWrappedObject(), new Marking())]) + ; + } + + function it_resolves_order_payment_state( + StateResolverInterface $stateResolver, + PaymentInterface $payment, + OrderInterface $order, + ): void { + $payment->getOrder()->willReturn($order); + + $stateResolver->resolve($order)->shouldBeCalled(); + + $this->__invoke(new CompletedEvent($payment->getWrappedObject(), new Marking())); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Shipment/AssignShippingDateListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Shipment/AssignShippingDateListenerSpec.php new file mode 100644 index 0000000000..a6896ee691 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Shipment/AssignShippingDateListenerSpec.php @@ -0,0 +1,47 @@ +beConstructedWith($shippingDateAssigner); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $subject): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new TransitionEvent($subject->getWrappedObject(), new Marking())]) + ; + } + + function it_resolves_order_state_after_order_being_shipped( + ShippingDateAssignerInterface $shippingDateAssigner, + ): void { + $shipment = new Shipment(); + $event = new TransitionEvent($shipment, new Marking()); + + $shippingDateAssigner->assign($shipment)->shouldBeCalled(); + + $this->__invoke($event); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Shipment/ResolveOrderShipmentStateListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Shipment/ResolveOrderShipmentStateListenerSpec.php new file mode 100644 index 0000000000..8d934ae192 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/EventListener/Workflow/Shipment/ResolveOrderShipmentStateListenerSpec.php @@ -0,0 +1,51 @@ +beConstructedWith($stateResolver); + } + + function it_throws_an_exception_on_non_supported_subject(\stdClass $subject): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new CompletedEvent($subject->getWrappedObject(), new Marking())]) + ; + } + + function it_resolves_order_shipment_state_after_order_being_shipped( + StateResolverInterface $stateResolver, + ): void { + $shipment = new Shipment(); + $order = new Order(); + $shipment->setOrder($order); + + $event = new CompletedEvent($shipment, new Marking()); + + $stateResolver->resolve($order)->shouldBeCalled(); + + $this->__invoke($event); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Mailer/AccountRegistrationEmailManagerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Mailer/AccountRegistrationEmailManagerSpec.php new file mode 100644 index 0000000000..b897a4f86d --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Mailer/AccountRegistrationEmailManagerSpec.php @@ -0,0 +1,57 @@ +beConstructedWith($sender); + } + + function it_implements_an_account_registration_email_manager_interface(): void + { + $this->shouldImplement(AccountRegistrationEmailManagerInterface::class); + } + + function it_sends_an_account_registration_email( + SenderInterface $sender, + UserInterface $user, + ChannelInterface $channel, + ): void { + $user->getEmail()->willReturn('customer@example.com'); + + $sender + ->send( + 'user_registration', + ['customer@example.com'], + [ + 'user' => $user, + 'localeCode' => 'en_US', + 'channel' => $channel, + ], + [], + ['customer@example.com'], + ) + ->shouldBeCalled(); + + $this->sendAccountRegistrationEmail($user, $channel, 'en_US'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Mailer/AccountVerificationEmailManagerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Mailer/AccountVerificationEmailManagerSpec.php new file mode 100644 index 0000000000..8a382aacfd --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Mailer/AccountVerificationEmailManagerSpec.php @@ -0,0 +1,55 @@ +beConstructedWith($sender); + } + + function it_implements_an_account_verification_email_manager_interface(): void + { + $this->shouldImplement(AccountVerificationEmailManagerInterface::class); + } + + function it_sends_an_account_verification_email( + SenderInterface $sender, + ChannelInterface $channel, + UserInterface $user, + ): void { + $user->getEmail()->willReturn('customer@example.com'); + + $sender + ->send( + 'account_verification', + ['customer@example.com'], + [ + 'user' => $user, + 'localeCode' => 'en_US', + 'channel' => $channel, + ], + ) + ->shouldBeCalled(); + + $this->sendAccountVerificationEmail($user, $channel, 'en_US'); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Mailer/ContactEmailManagerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Mailer/ContactEmailManagerSpec.php new file mode 100644 index 0000000000..f2bf6790df --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Mailer/ContactEmailManagerSpec.php @@ -0,0 +1,67 @@ +beConstructedWith($sender); + } + + function it_implements_a_contact_email_manager_interface(): void + { + $this->shouldImplement(ContactEmailManagerInterface::class); + } + + function it_sends_a_contact_request_email( + SenderInterface $sender, + ChannelInterface $channel, + ): void { + $sender + ->send( + 'contact_request', + ['contact@example.com'], + [ + 'data' => [ + 'email' => 'customer@example.com', + 'message' => 'Hello!', + ], + 'channel' => $channel, + 'localeCode' => 'en_US', + ], + [], + ['customer@example.com'], + ) + ->shouldBeCalled() + ; + + $this + ->sendContactRequest( + [ + 'email' => 'customer@example.com', + 'message' => 'Hello!', + ], + ['contact@example.com'], + $channel, + 'en_US', + ) + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Mailer/ShipmentEmailManagerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Mailer/ShipmentEmailManagerSpec.php new file mode 100644 index 0000000000..f558c2086c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Mailer/ShipmentEmailManagerSpec.php @@ -0,0 +1,87 @@ +beConstructedWith($sender); + } + + function it_implements_a_shipment_email_manager_interface(): void + { + $this->shouldImplement(ShipmentEmailManagerInterface::class); + } + + function it_sends_a_shipment_confirmation_email( + SenderInterface $sender, + ShipmentInterface $shipment, + OrderInterface $order, + ChannelInterface $channel, + CustomerInterface $customer, + ): void { + $shipment->getOrder()->willReturn($order); + $order->getChannel()->willReturn($channel); + $order->getLocaleCode()->willReturn('en_US'); + $order->getCustomer()->willReturn($customer); + $customer->getEmail()->willReturn('customer@example.com'); + + $sender + ->send('shipment_confirmation', ['customer@example.com'], [ + 'shipment' => $shipment, + 'order' => $order, + 'channel' => $channel, + 'localeCode' => 'en_US', + ]) + ->shouldBeCalled() + ; + + $this->sendConfirmationEmail($shipment); + } + + function it_resends_a_shipment_confirmation_email( + SenderInterface $sender, + ShipmentInterface $shipment, + OrderInterface $order, + ChannelInterface $channel, + CustomerInterface $customer, + ): void { + $shipment->getOrder()->willReturn($order); + $order->getChannel()->willReturn($channel); + $order->getLocaleCode()->willReturn('en_US'); + $order->getCustomer()->willReturn($customer); + $customer->getEmail()->willReturn('customer@example.com'); + + $sender + ->send('shipment_confirmation_resent', ['customer@example.com'], [ + 'shipment' => $shipment, + 'order' => $order, + 'channel' => $channel, + 'localeCode' => 'en_US', + ]) + ->shouldBeCalled() + ; + + $this->resendConfirmationEmail($shipment); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/MessageDispatcher/ResendOrderConfirmationEmailDispatcherSpec.php b/src/Sylius/Bundle/CoreBundle/spec/MessageDispatcher/ResendOrderConfirmationEmailDispatcherSpec.php new file mode 100644 index 0000000000..03e3bc0cc5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/MessageDispatcher/ResendOrderConfirmationEmailDispatcherSpec.php @@ -0,0 +1,40 @@ +beConstructedWith($messageBus); + } + + function it_dispatches_a_resend_confirmation_email( + MessageBusInterface $messageBus, + OrderInterface $order, + ): void { + $order->getTokenValue()->willReturn('token'); + $message = new ResendOrderConfirmationEmail('token'); + + $messageBus->dispatch($message)->willReturn(new Envelope($message))->shouldBeCalled(); + + $this->dispatch($order); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/MessageDispatcher/ResendShipmentConfirmationEmailDispatcherSpec.php b/src/Sylius/Bundle/CoreBundle/spec/MessageDispatcher/ResendShipmentConfirmationEmailDispatcherSpec.php new file mode 100644 index 0000000000..b386d12c31 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/MessageDispatcher/ResendShipmentConfirmationEmailDispatcherSpec.php @@ -0,0 +1,40 @@ +beConstructedWith($messageBus); + } + + function it_dispatches_a_resend_confirmation_email( + MessageBusInterface $messageBus, + ShipmentInterface $shipment, + ): void { + $shipment->getId()->willReturn(12); + $message = new ResendShipmentConfirmationEmail(12); + + $messageBus->dispatch($message)->willReturn(new Envelope($message))->shouldBeCalled(); + + $this->dispatch($shipment); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/MessageHandler/Admin/Account/SendResetPasswordEmailHandlerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/MessageHandler/Admin/Account/SendResetPasswordEmailHandlerSpec.php index 31fe4534df..b1eb565d86 100644 --- a/src/Sylius/Bundle/CoreBundle/spec/MessageHandler/Admin/Account/SendResetPasswordEmailHandlerSpec.php +++ b/src/Sylius/Bundle/CoreBundle/spec/MessageHandler/Admin/Account/SendResetPasswordEmailHandlerSpec.php @@ -14,10 +14,10 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\CoreBundle\MessageHandler\Admin\Account; use PhpSpec\ObjectBehavior; -use Sylius\Bundle\CoreBundle\Mailer\Emails; +use Prophecy\Argument; +use Sylius\Bundle\CoreBundle\Mailer\ResetPasswordEmailManagerInterface; use Sylius\Bundle\CoreBundle\Message\Admin\Account\SendResetPasswordEmail; use Sylius\Component\Core\Model\AdminUserInterface; -use Sylius\Component\Mailer\Sender\SenderInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; @@ -25,9 +25,9 @@ final class SendResetPasswordEmailHandlerSpec extends ObjectBehavior { public function let( UserRepositoryInterface $userRepository, - SenderInterface $sender, + ResetPasswordEmailManagerInterface $resetPasswordEmailManager, ): void { - $this->beConstructedWith($userRepository, $sender); + $this->beConstructedWith($userRepository, $resetPasswordEmailManager); } public function it_is_a_message_handler(): void @@ -37,28 +37,26 @@ final class SendResetPasswordEmailHandlerSpec extends ObjectBehavior public function it_handles_sending_reset_password_email( UserRepositoryInterface $userRepository, - SenderInterface $sender, + ResetPasswordEmailManagerInterface $resetPasswordEmailManager, AdminUserInterface $adminUser, ): void { $userRepository->findOneByEmail('admin@example.com')->willReturn($adminUser); - $sender->send( - Emails::ADMIN_PASSWORD_RESET, - ['admin@example.com'], - [ - 'adminUser' => $adminUser, - 'localeCode' => 'en_US', - ], - )->shouldBeCalledOnce(); + $resetPasswordEmailManager->sendResetPasswordEmail($adminUser, 'en_US')->shouldNotBeCalled(); + $resetPasswordEmailManager->sendAdminResetPasswordEmail($adminUser, 'en_US')->shouldBeCalledOnce(); $this(new SendResetPasswordEmail('admin@example.com', 'en_US')); } public function it_throws_exception_while_handling_if_user_doesnt_exist( UserRepositoryInterface $userRepository, + ResetPasswordEmailManagerInterface $resetPasswordEmailManager, ): void { $userRepository->findOneByEmail('admin@example.com')->willReturn(null); + $resetPasswordEmailManager->sendResetPasswordEmail(Argument::cetera())->shouldNotBeCalled(); + $resetPasswordEmailManager->sendAdminResetPasswordEmail(Argument::cetera())->shouldNotBeCalled(); + $this ->shouldThrow(\InvalidArgumentException::class) ->during('__invoke', [new SendResetPasswordEmail('admin@example.com', 'en_US')]) diff --git a/src/Sylius/Bundle/CoreBundle/spec/MessageHandler/ResendOrderConfirmationEmailHandlerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/MessageHandler/ResendOrderConfirmationEmailHandlerSpec.php new file mode 100644 index 0000000000..2288e12048 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/MessageHandler/ResendOrderConfirmationEmailHandlerSpec.php @@ -0,0 +1,58 @@ +beConstructedWith($orderEmailManager, $orderRepository); + } + + function it_is_a_message_handler(): void + { + $this->shouldImplement(MessageHandlerInterface::class); + } + + function it_resends_order_confirmation_email( + OrderEmailManagerInterface $orderEmailManager, + RepositoryInterface $orderRepository, + ): void { + $order = new Order(); + + $orderRepository->findOneBy(['tokenValue' => 'TOKEN'])->willReturn($order); + $orderEmailManager->resendConfirmationEmail($order)->shouldBeCalled(); + + $this->__invoke(new ResendOrderConfirmationEmail('TOKEN')); + } + + function it_throws_not_found_exception_when_order_not_found( + RepositoryInterface $orderRepository, + ): void { + $orderRepository->findOneBy(['tokenValue' => 'NON_EXISTING_TOKEN'])->willReturn(null); + + $this + ->shouldThrow(NotFoundHttpException::class) + ->during('__invoke', [new ResendOrderConfirmationEmail('NON_EXISTING_TOKEN')]) + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/MessageHandler/ResendShipmentConfirmationEmailHandlerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/MessageHandler/ResendShipmentConfirmationEmailHandlerSpec.php new file mode 100644 index 0000000000..547126f22f --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/MessageHandler/ResendShipmentConfirmationEmailHandlerSpec.php @@ -0,0 +1,57 @@ +beConstructedWith($shipmentRepository, $shipmentEmailManager); + } + + function it_is_a_message_handler(): void + { + $this->shouldImplement(MessageHandlerInterface::class); + } + + function it_resends_shipment_confirmation_email( + ShipmentEmailManagerInterface $shipmentEmailManager, + RepositoryInterface $shipmentRepository, + ShipmentInterface $shipment, + ): void { + $shipmentRepository->find('12')->willReturn($shipment); + $shipmentEmailManager->resendConfirmationEmail($shipment)->shouldBeCalled(); + + $this->__invoke(new ResendShipmentConfirmationEmail(12)); + } + + function it_throws_not_found_exception_when_shipment_not_found( + RepositoryInterface $shipmentRepository, + ): void { + $shipmentRepository->find('10')->willReturn(null); + + $this + ->shouldThrow(NotFoundHttpException::class) + ->during('__invoke', [new ResendShipmentConfirmationEmail(10)]) + ; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/CommandDispatcher/BatchedApplyLowestPriceOnChannelPricingsCommandDispatcherSpec.php b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/CommandDispatcher/BatchedApplyLowestPriceOnChannelPricingsCommandDispatcherSpec.php new file mode 100644 index 0000000000..3fc550251c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/CommandDispatcher/BatchedApplyLowestPriceOnChannelPricingsCommandDispatcherSpec.php @@ -0,0 +1,90 @@ +beConstructedWith($channelPricingRepository, $commandBus, 2); + } + + function it_implements_apply_lowest_price_on_channel_pricings_command_dispatcher_interface(): void + { + $this->shouldImplement(ApplyLowestPriceOnChannelPricingsCommandDispatcherInterface::class); + } + + function it_dispatches_applications_of_lowest_price_on_channel_pricing_within_channel_in_batches( + RepositoryInterface $channelPricingRepository, + MessageBusInterface $commandBus, + ChannelInterface $channel, + ChannelPricingInterface $firstChannelPricing, + ChannelPricingInterface $secondChannelPricing, + ChannelPricingInterface $thirdChannelPricing, + ChannelPricingInterface $fourthChannelPricing, + ChannelPricingInterface $fifthChannelPricing, + ): void { + $channel->getCode()->willReturn('WEB'); + + $firstChannelPricing->getId()->willReturn(1); + $secondChannelPricing->getId()->willReturn(2); + $thirdChannelPricing->getId()->willReturn(6); + $fourthChannelPricing->getId()->willReturn(7); + $fifthChannelPricing->getId()->willReturn(9); + + $batches = [ + [ + $firstChannelPricing->getWrappedObject(), + $secondChannelPricing->getWrappedObject(), + ], + [ + $thirdChannelPricing->getWrappedObject(), + $fourthChannelPricing->getWrappedObject(), + ], + [ + $fifthChannelPricing->getWrappedObject(), + ], + [], + ]; + + $batchSize = 2; + + foreach ($batches as $key => $batch) { + $channelPricingRepository + ->findBy(['channelCode' => 'WEB'], ['id' => 'ASC'], 2, $key * $batchSize) + ->willReturn($batch) + ->shouldBeCalled() + ; + } + + foreach ([[1, 2], [6, 7], [9]] as $ids) { + $commandBus + ->dispatch($command = new ApplyLowestPriceOnChannelPricings($ids)) + ->willReturn(new Envelope($command)) + ->shouldBeCalled() + ; + } + + $this->applyWithinChannel($channel); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/CommandHandler/ApplyLowestPriceOnChannelPricingsHandlerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/CommandHandler/ApplyLowestPriceOnChannelPricingsHandlerSpec.php new file mode 100644 index 0000000000..0e732e5d27 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/CommandHandler/ApplyLowestPriceOnChannelPricingsHandlerSpec.php @@ -0,0 +1,75 @@ +beConstructedWith($productLowestPriceBeforeDiscountProcessor, $channelPricingRepository); + } + + function it_is_initializable(): void + { + $this->shouldHaveType(ApplyLowestPriceOnChannelPricingsHandler::class); + } + + function it_applies_lowest_price_before_discount_on_all_given_channel_pricings( + ProductLowestPriceBeforeDiscountProcessorInterface $productLowestPriceBeforeDiscountProcessor, + RepositoryInterface $channelPricingRepository, + ChannelPricingInterface $firstChannelPricing, + ChannelPricingInterface $secondChannelPricing, + ChannelPricingInterface $thirdChannelPricing, + ): void { + $channelPricingRepository + ->findBy(['id' => [1, 3, 4]]) + ->willReturn([ + $firstChannelPricing->getWrappedObject(), + $secondChannelPricing->getWrappedObject(), + $thirdChannelPricing->getWrappedObject(), + ]) + ; + + $productLowestPriceBeforeDiscountProcessor->process($firstChannelPricing)->shouldBeCalled(); + $productLowestPriceBeforeDiscountProcessor->process($secondChannelPricing)->shouldBeCalled(); + $productLowestPriceBeforeDiscountProcessor->process($thirdChannelPricing)->shouldBeCalled(); + $productLowestPriceBeforeDiscountProcessor->process(Argument::any())->shouldBeCalledTimes(3); + + $this(new ApplyLowestPriceOnChannelPricings([1, 3, 4])); + } + + function it_does_not_apply_lowest_price_before_discount_on_any_channel_pricing_if_there_are_no_given_channel_pricings( + ProductLowestPriceBeforeDiscountProcessorInterface $productLowestPriceBeforeDiscountProcessor, + RepositoryInterface $channelPricingRepository, + ): void { + $channelPricingRepository + ->findBy(['id' => []]) + ->willReturn([]) + ; + + $productLowestPriceBeforeDiscountProcessor->process(Argument::any())->shouldNotBeCalled(); + + $this(new ApplyLowestPriceOnChannelPricings([])); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EntityObserver/CreateLogEntryOnPriceChangeObserverSpec.php b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EntityObserver/CreateLogEntryOnPriceChangeObserverSpec.php new file mode 100644 index 0000000000..a3d09c0975 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EntityObserver/CreateLogEntryOnPriceChangeObserverSpec.php @@ -0,0 +1,69 @@ +beConstructedWith($priceChangeLogger); + } + + function it_implements_on_entity_change_interface(): void + { + $this->shouldImplement(EntityObserverInterface::class); + } + + function it_supports_channel_pricing_with_price_specified_only( + ChannelPricingInterface $channelPricingWithPrice, + ChannelPricingInterface $channelPricingWithoutPrice, + OrderInterface $order, + ): void { + $channelPricingWithPrice->getPrice()->willReturn(1000); + + $this->supports($channelPricingWithPrice)->shouldReturn(true); + $this->supports($channelPricingWithoutPrice)->shouldReturn(false); + $this->supports($order)->shouldReturn(false); + } + + function it_supports_price_and_original_price_fields(): void + { + $this->observedFields()->shouldReturn(['price', 'originalPrice']); + } + + function it_logs_price_change( + PriceChangeLoggerInterface $priceChangeLogger, + ChannelPricingInterface $channelPricing, + ): void { + $priceChangeLogger->log($channelPricing)->shouldBeCalled(); + + $this->onChange($channelPricing); + } + + function it_throws_an_error_if_entity_is_not_channel_pricing( + PriceChangeLoggerInterface $priceChangeLogger, + ChannelInterface $channel, + ): void { + $priceChangeLogger->log($channel)->shouldNotBeCalled(); + + $this->shouldThrow(\InvalidArgumentException::class)->during('onChange', [$channel]); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelChangeObserverSpec.php b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelChangeObserverSpec.php new file mode 100644 index 0000000000..6402dd8577 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelChangeObserverSpec.php @@ -0,0 +1,108 @@ +beConstructedWith($commandDispatcher); + } + + function it_is_an_entity_observer(): void + { + $this->shouldImplement(EntityObserverInterface::class); + } + + function it_does_not_support_anything_other_than_channel_interface(OrderInterface $order): void + { + $this->supports($order)->shouldReturn(false); + } + + function it_does_not_support_a_channel_that_is_currently_being_processed( + ChannelInterface $channel, + ): void { + $channel->getCode()->willReturn('test'); + $channel->getChannelPriceHistoryConfig()->shouldNotBeCalled(); + + $object = $this->object->getWrappedObject(); + $objectReflection = new \ReflectionObject($object); + $property = $objectReflection->getProperty('channelsCurrentlyProcessed'); + $property->setAccessible(true); + $property->setValue($object, ['test' => true]); + + $this->supports($channel)->shouldReturn(false); + } + + function it_does_not_support_channels_with_no_price_history_config(ChannelInterface $channel): void + { + $channel->getCode()->willReturn('test'); + $channel->getChannelPriceHistoryConfig()->willReturn(null); + + $this->supports($channel)->shouldReturn(false); + } + + function it_does_not_support_channels_with_existing_price_history_config( + ChannelInterface $channel, + ChannelPriceHistoryConfigInterface $config, + ): void { + $channel->getCode()->willReturn('test'); + $channel->getChannelPriceHistoryConfig()->willReturn($config); + $config->getId()->willReturn(12); + + $this->supports($channel)->shouldReturn(false); + } + + function it_only_supports_channels_with_new_price_history_config( + ChannelInterface $channel, + ChannelPriceHistoryConfigInterface $config, + ): void { + $channel->getCode()->willReturn('test'); + $channel->getChannelPriceHistoryConfig()->willReturn($config); + $config->getId()->willReturn(null); + + $this->supports($channel)->shouldReturn(true); + } + + function it_observes_channel_price_history_config_field(): void + { + $this->observedFields()->shouldReturn(['channelPriceHistoryConfig']); + } + + function it_delegates_processing_lowest_prices_to_command_dispatcher( + ApplyLowestPriceOnChannelPricingsCommandDispatcherInterface $commandDispatcher, + ChannelInterface $channel, + ): void { + $commandDispatcher->applyWithinChannel($channel)->shouldBeCalled(); + + $this->onChange($channel); + } + + function it_throws_an_exception_if_entity_is_not_channel( + ApplyLowestPriceOnChannelPricingsCommandDispatcherInterface $commandDispatcher, + OrderInterface $order, + ): void { + $commandDispatcher->applyWithinChannel(Argument::any())->shouldNotBeCalled(); + + $this->shouldThrow(\InvalidArgumentException::class)->during('onChange', [$order]); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserverSpec.php b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserverSpec.php new file mode 100644 index 0000000000..d89b07865b --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EntityObserver/ProcessLowestPricesOnChannelPriceHistoryConfigChangeObserverSpec.php @@ -0,0 +1,113 @@ +beConstructedWith($channelRepository, $commandDispatcher); + } + + function it_is_an_entity_observer(): void + { + $this->shouldImplement(EntityObserverInterface::class); + } + + function it_does_not_support_anything_other_than_channel_price_history_config_interface( + OrderInterface $order, + ): void { + $this->supports($order)->shouldReturn(false); + } + + function it_does_not_support_new_configs(ChannelPriceHistoryConfigInterface $config): void + { + $config->getId()->willReturn(null); + + $this->supports($config)->shouldReturn(false); + } + + function it_only_supports_existing_configs(ChannelPriceHistoryConfigInterface $config): void + { + $config->getId()->willReturn(1); + + $this->supports($config)->shouldReturn(true); + } + + function it_does_not_support_a_config_that_is_currently_being_processed( + ChannelPriceHistoryConfigInterface $config, + ): void { + $config->getId()->willReturn(1); + + $object = $this->object->getWrappedObject(); + $objectReflection = new \ReflectionObject($object); + $property = $objectReflection->getProperty('configsCurrentlyProcessed'); + $property->setAccessible(true); + $property->setValue($object, [1 => true]); + + $this->supports($config)->shouldReturn(false); + } + + function it_observes_lowest_price_for_discounted_products_checking_period_field(): void + { + $this->observedFields()->shouldReturn(['lowestPriceForDiscountedProductsCheckingPeriod']); + } + + function it_throws_an_exception_when_entity_is_not_a_channel_price_history_config_interface( + ChannelRepositoryInterface $channelRepository, + ApplyLowestPriceOnChannelPricingsCommandDispatcherInterface $commandDispatcher, + OrderInterface $order, + ): void { + $channelRepository->findOneBy(Argument::any())->shouldNotBeCalled(); + $commandDispatcher->applyWithinChannel(Argument::any())->shouldNotBeCalled(); + + $this->shouldThrow(\InvalidArgumentException::class)->during('onChange', [$order]); + } + + function it_does_nothing_when_config_has_no_channel_counterpart( + ChannelRepositoryInterface $channelRepository, + ApplyLowestPriceOnChannelPricingsCommandDispatcherInterface $commandDispatcher, + ChannelPriceHistoryConfigInterface $config, + ): void { + $channelRepository->findOneBy(['channelPriceHistoryConfig' => $config])->willReturn(null); + + $commandDispatcher->applyWithinChannel(Argument::any())->shouldNotBeCalled(); + + $this->onChange($config); + } + + function it_delegates_processing_lowest_prices_to_command_dispatcher( + ChannelRepositoryInterface $channelRepository, + ApplyLowestPriceOnChannelPricingsCommandDispatcherInterface $commandDispatcher, + ChannelInterface $channel, + ChannelPriceHistoryConfigInterface $config, + ): void { + $channelRepository->findOneBy(['channelPriceHistoryConfig' => $config])->willReturn($channel); + + $commandDispatcher->applyWithinChannel($channel)->shouldBeCalled(); + + $this->onChange($config); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EventListener/ChannelPricingLogEntryEventListenerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EventListener/ChannelPricingLogEntryEventListenerSpec.php new file mode 100644 index 0000000000..1669788a6b --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/EventListener/ChannelPricingLogEntryEventListenerSpec.php @@ -0,0 +1,50 @@ +beConstructedWith($lowestPriceProcessor); + } + + function it_does_nothing_when_object_is_not_channel_pricing_log_entry( + LifecycleEventArgs $event, + ChannelPricingInterface $channelPricing, + ): void { + $event->getObject()->willReturn($channelPricing); + + $this->postPersist($event); + } + + function it_processes_lowest_price_for_channel_pricing( + ProductLowestPriceBeforeDiscountProcessorInterface $lowestPriceProcessor, + ChannelPricingInterface $channelPricing, + ChannelPricingLogEntryInterface $channelPricingLogEntry, + LifecycleEventArgs $event, + ): void { + $event->getObject()->willReturn($channelPricingLogEntry); + $channelPricingLogEntry->getChannelPricing()->willReturn($channelPricing); + $lowestPriceProcessor->process($channelPricing)->shouldBeCalled(); + + $this->postPersist($event); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/Logger/PriceChangeLoggerSpec.php b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/Logger/PriceChangeLoggerSpec.php new file mode 100644 index 0000000000..cbc4d4d4dd --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/Logger/PriceChangeLoggerSpec.php @@ -0,0 +1,81 @@ +beConstructedWith($logEntryFactory, $logEntryManager, $dateTimeProvider); + } + + function it_implements_price_change_logger_interface(): void + { + $this->shouldImplement(PriceChangeLoggerInterface::class); + } + + function it_throws_exception_when_there_is_no_price( + ChannelPricingLogEntryFactoryInterface $logEntryFactory, + ObjectManager $logEntryManager, + DateTimeProviderInterface $dateTimeProvider, + ChannelPricingInterface $channelPricing, + ): void { + $channelPricing->getPrice()->willReturn(null); + + $dateTimeProvider->now()->shouldNotBeCalled(); + $logEntryFactory->create(Argument::cetera())->shouldNotBeCalled(); + $logEntryManager->persist(Argument::any())->shouldNotBeCalled(); + + $this->shouldThrow(\InvalidArgumentException::class)->during('log', [$channelPricing]); + } + + function it_logs_price_change( + ChannelPricingLogEntryFactoryInterface $logEntryFactory, + ObjectManager $logEntryManager, + DateTimeProviderInterface $dateTimeProvider, + ChannelPricingInterface $channelPricing, + ChannelPricingLogEntryInterface $logEntry, + ): void { + $date = new \DateTimeImmutable(); + $price = 1000; + $originalPrice = 1200; + + $channelPricing->getPrice()->willReturn($price); + $channelPricing->getOriginalPrice()->willReturn($originalPrice); + + $dateTimeProvider->now()->willReturn($date); + + $logEntryFactory + ->create($channelPricing, $date, $price, $originalPrice) + ->shouldBeCalled() + ->willReturn($logEntry) + ; + + $logEntryManager->persist($logEntry)->shouldBeCalled(); + + $this->log($channelPricing); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/Processor/ProductLowestPriceBeforeDiscountProcessorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/Processor/ProductLowestPriceBeforeDiscountProcessorSpec.php new file mode 100644 index 0000000000..ca5cb74270 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/Processor/ProductLowestPriceBeforeDiscountProcessorSpec.php @@ -0,0 +1,153 @@ +beConstructedWith($channelPricingLogEntryRepository, $channelRepository); + } + + function it_implements_product_lowest_price_processor_interface(): void + { + $this->shouldImplement(ProductLowestPriceBeforeDiscountProcessorInterface::class); + } + + function it_sets_lowest_price_before_discount_to_null_if_original_price_is_null( + ChannelPricingLogEntryRepositoryInterface $channelPricingLogEntryRepository, + ChannelRepositoryInterface $channelRepository, + ChannelPricingInterface $channelPricing, + ): void { + $channelPricing->getOriginalPrice()->willReturn(null); + $channelPricing->getPrice()->willReturn(2100); + + $channelRepository->findOneByCode(Argument::any())->shouldNotBeCalled(); + $channelPricingLogEntryRepository->findLatestOneByChannelPricing($channelPricing)->shouldNotBeCalled(); + $channelPricingLogEntryRepository->findLowestPriceInPeriod(Argument::cetera())->shouldNotBeCalled(); + + $channelPricing->getChannelCode()->shouldNotBeCalled(); + $channelPricing->setLowestPriceBeforeDiscount(null)->shouldBeCalled(); + + $this->process($channelPricing); + } + + function it_sets_lowest_price_before_discount_to_null_if_price_is_equal_original_price( + ChannelPricingLogEntryRepositoryInterface $channelPricingLogEntryRepository, + ChannelRepositoryInterface $channelRepository, + ChannelPricingInterface $channelPricing, + ): void { + $channelPricing->getOriginalPrice()->willReturn(2100); + $channelPricing->getPrice()->willReturn(2100); + + $channelRepository->findOneByCode(Argument::any())->shouldNotBeCalled(); + $channelPricingLogEntryRepository->findLatestOneByChannelPricing($channelPricing)->shouldNotBeCalled(); + $channelPricingLogEntryRepository->findLowestPriceInPeriod(Argument::cetera())->shouldNotBeCalled(); + + $channelPricing->getChannelCode()->shouldNotBeCalled(); + $channelPricing->setLowestPriceBeforeDiscount(null)->shouldBeCalled(); + + $this->process($channelPricing); + } + + function it_sets_lowest_price_before_discount_to_null_if_price_is_greater_than_original_price( + ChannelPricingLogEntryRepositoryInterface $channelPricingLogEntryRepository, + ChannelRepositoryInterface $channelRepository, + ChannelPricingInterface $channelPricing, + ): void { + $channelPricing->getOriginalPrice()->willReturn(2100); + $channelPricing->getPrice()->willReturn(3700); + + $channelRepository->findOneByCode(Argument::any())->shouldNotBeCalled(); + $channelPricingLogEntryRepository->findLatestOneByChannelPricing($channelPricing)->shouldNotBeCalled(); + $channelPricingLogEntryRepository->findLowestPriceInPeriod(Argument::cetera())->shouldNotBeCalled(); + + $channelPricing->getChannelCode()->shouldNotBeCalled(); + $channelPricing->setLowestPriceBeforeDiscount(null)->shouldBeCalled(); + + $this->process($channelPricing); + } + + function it_sets_lowest_price_before_discount_to_null_if_there_is_no_log_entries( + ChannelPricingLogEntryRepositoryInterface $channelPricingLogEntryRepository, + ChannelRepositoryInterface $channelRepository, + ChannelInterface $channel, + ChannelPriceHistoryConfigInterface $channelPriceHistoryConfig, + ChannelPricingInterface $channelPricing, + ): void { + $channel->getChannelPriceHistoryConfig()->willReturn($channelPriceHistoryConfig); + + $channelPricing->getOriginalPrice()->willReturn(3700); + $channelPricing->getPrice()->willReturn(2100); + $channelPricing->getChannelCode()->willReturn('WEB'); + $channelPriceHistoryConfig->getLowestPriceForDiscountedProductsCheckingPeriod()->shouldNotBeCalled(); + + $channelRepository->findOneByCode('WEB')->willReturn($channel); + $channelPricingLogEntryRepository->findLatestOneByChannelPricing($channelPricing)->willReturn(null); + $channelPricingLogEntryRepository->findLowestPriceInPeriod(Argument::cetera())->shouldNotBeCalled(); + + $channelPricing->setLowestPriceBeforeDiscount(null)->shouldBeCalled(); + + $this->process($channelPricing); + } + + function it_sets_lowest_price_before_discount_to_lowest_price_found_in_the_given_period_if_price_is_less_than_original_price( + ChannelPricingLogEntryRepositoryInterface $channelPricingLogEntryRepository, + ChannelRepositoryInterface $channelRepository, + ChannelInterface $channel, + ChannelPriceHistoryConfigInterface $channelPriceHistoryConfig, + ChannelPricingInterface $channelPricing, + ChannelPricingLogEntry $latestLogEntry, + ): void { + $channel->getChannelPriceHistoryConfig()->willReturn($channelPriceHistoryConfig); + + $channelPricing->getOriginalPrice()->willReturn(3700); + $channelPricing->getPrice()->willReturn(2100); + $channelPricing->getChannelCode()->willReturn('WEB'); + + $channelRepository->findOneByCode('WEB')->willReturn($channel); + $channelPriceHistoryConfig->getLowestPriceForDiscountedProductsCheckingPeriod()->willReturn(30); + + $unformattedDate = new \DateTimeImmutable(); + $latestLogEntry->getLoggedAt()->willReturn($unformattedDate); + $loggedAt = new \DateTimeImmutable($unformattedDate->format('Y-m-d H:i:s')); + $startDate = $loggedAt->sub(new \DateInterval(sprintf('P%dD', 30))); + + $latestLogEntry->getChannelPricing()->willReturn($channelPricing); + $latestLogEntry->getLoggedAt()->willReturn($loggedAt); + $latestLogEntry->getId()->willReturn(1234); + + $channelPricingLogEntryRepository->findLatestOneByChannelPricing($channelPricing)->willReturn($latestLogEntry); + $channelPricingLogEntryRepository + ->findLowestPriceInPeriod(1234, $channelPricing, $startDate) + ->willReturn(6900) + ; + + $channelPricing->setLowestPriceBeforeDiscount(6900)->shouldBeCalled(); + + $this->process($channelPricing); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/Remover/ChannelPricingLogEntriesRemoverSpec.php b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/Remover/ChannelPricingLogEntriesRemoverSpec.php new file mode 100644 index 0000000000..329be36a2c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/PriceHistory/Remover/ChannelPricingLogEntriesRemoverSpec.php @@ -0,0 +1,145 @@ +beConstructedWith( + $channelPricingLogEntriesRepository, + $manager, + $dateTimeProvider, + $eventDispatcher, + self::BATCH_SIZE, + ); + } + + function it_implements_channel_pricing_log_entries_remover_interface(): void + { + $this->shouldImplement(ChannelPricingLogEntriesRemover::class); + } + + function it_does_nothing_when_no_log_entries_were_found( + ChannelPricingLogEntryRepositoryInterface $channelPricingLogEntriesRepository, + ObjectManager $manager, + DateTimeProviderInterface $dateTimeProvider, + EventDispatcherInterface $eventDispatcher, + ): void { + $date = new \DateTimeImmutable(); + $dateTimeProvider->now()->willReturn($date); + + $channelPricingLogEntriesRepository->findOlderThan($date->modify('-1 days'), self::BATCH_SIZE)->willReturn([]); + + $manager->remove(Argument::any())->shouldNotBeCalled(); + $manager->flush()->shouldNotBeCalled(); + $manager->clear()->shouldNotBeCalled(); + + $eventDispatcher->dispatch(Argument::cetera())->shouldNotBeCalled(); + + $this->remove(1); + } + + function it_removes_a_single_batch_of_channel_pricing_log_entries_when_there_is_no_more( + ChannelPricingLogEntryRepositoryInterface $channelPricingLogEntriesRepository, + ObjectManager $manager, + DateTimeProviderInterface $dateTimeProvider, + EventDispatcherInterface $eventDispatcher, + ChannelPricingLogEntryInterface $channelPricingLogEntry, + ): void { + $date = new \DateTimeImmutable(); + $dateTimeProvider->now()->willReturn($date); + + $channelPricingLogEntriesRepository + ->findOlderThan($date->modify('-1 days'), self::BATCH_SIZE) + ->willReturn([$channelPricingLogEntry], []) + ; + + $manager->remove($channelPricingLogEntry)->shouldBeCalled(); + + $eventDispatcher->dispatch( + new GenericEvent([$channelPricingLogEntry->getWrappedObject()]), + OldChannelPricingLogEntriesEvents::PRE_REMOVE, + )->shouldBeCalled(); + $manager->flush()->shouldBeCalled(); + + $eventDispatcher->dispatch( + new GenericEvent([$channelPricingLogEntry->getWrappedObject()]), + OldChannelPricingLogEntriesEvents::POST_REMOVE, + )->shouldBeCalled(); + $manager->clear()->shouldBeCalled(); + + $this->remove(1); + } + + function it_removes_multiple_batches_of_channel_pricing_log_entries( + ChannelPricingLogEntryRepositoryInterface $channelPricingLogEntriesRepository, + ObjectManager $manager, + DateTimeProviderInterface $dateTimeProvider, + EventDispatcherInterface $eventDispatcher, + ChannelPricingLogEntryInterface $firstChannelPricingLogEntry, + ChannelPricingLogEntryInterface $secondChannelPricingLogEntry, + ): void { + $date = new \DateTimeImmutable(); + $dateTimeProvider->now()->willReturn($date); + + $channelPricingLogEntriesRepository + ->findOlderThan($date->modify('-1 days'), self::BATCH_SIZE) + ->willReturn([$firstChannelPricingLogEntry], [$secondChannelPricingLogEntry], []) + ; + + $eventDispatcher->dispatch( + new GenericEvent([$firstChannelPricingLogEntry->getWrappedObject()]), + OldChannelPricingLogEntriesEvents::PRE_REMOVE, + )->shouldBeCalledTimes(1); + $eventDispatcher->dispatch( + new GenericEvent([$secondChannelPricingLogEntry->getWrappedObject()]), + OldChannelPricingLogEntriesEvents::PRE_REMOVE, + )->shouldBeCalledTimes(1); + + $manager->remove($firstChannelPricingLogEntry)->shouldBeCalledTimes(1); + $manager->remove($secondChannelPricingLogEntry)->shouldBeCalledTimes(1); + + $eventDispatcher->dispatch( + new GenericEvent([$firstChannelPricingLogEntry->getWrappedObject()]), + OldChannelPricingLogEntriesEvents::POST_REMOVE, + )->shouldBeCalledTimes(1); + $eventDispatcher->dispatch( + new GenericEvent([$secondChannelPricingLogEntry->getWrappedObject()]), + OldChannelPricingLogEntriesEvents::POST_REMOVE, + )->shouldBeCalledTimes(1); + + $manager->flush()->shouldBeCalledTimes(2); + $manager->clear()->shouldBeCalledTimes(2); + + $this->remove(1); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Security/UserPasswordResetterSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Security/UserPasswordResetterSpec.php index dd6a8e0c8b..4317591e1c 100644 --- a/src/Sylius/Bundle/CoreBundle/spec/Security/UserPasswordResetterSpec.php +++ b/src/Sylius/Bundle/CoreBundle/spec/Security/UserPasswordResetterSpec.php @@ -16,6 +16,7 @@ namespace spec\Sylius\Bundle\CoreBundle\Security; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Sylius\Bundle\CoreBundle\Security\UserPasswordResetterInterface; +use Sylius\Bundle\UserBundle\Exception\UserNotFoundException; use Sylius\Component\User\Model\UserInterface; use Sylius\Component\User\Repository\UserRepositoryInterface; use Sylius\Component\User\Security\PasswordUpdaterInterface; @@ -62,7 +63,7 @@ final class UserPasswordResetterSpec extends ObjectBehavior $passwordUpdater->updatePassword(Argument::any())->shouldNotBeCalled(); $this - ->shouldThrow(\InvalidArgumentException::class) + ->shouldThrow(UserNotFoundException::class) ->during('reset', ['TOKEN', 'newPassword']) ; } diff --git a/src/Sylius/Bundle/CoreBundle/spec/Templating/Helper/PriceHelperSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Templating/Helper/PriceHelperSpec.php index e565eb25bc..48d000dc88 100644 --- a/src/Sylius/Bundle/CoreBundle/spec/Templating/Helper/PriceHelperSpec.php +++ b/src/Sylius/Bundle/CoreBundle/spec/Templating/Helper/PriceHelperSpec.php @@ -21,9 +21,9 @@ use Symfony\Component\Templating\Helper\Helper; final class PriceHelperSpec extends ObjectBehavior { - function let(ProductVariantPricesCalculatorInterface $productVariantPriceCalculator): void + function let(ProductVariantPricesCalculatorInterface $productVariantPricesCalculator): void { - $this->beConstructedWith($productVariantPriceCalculator); + $this->beConstructedWith($productVariantPricesCalculator); } function it_is_helper(): void @@ -32,91 +32,171 @@ final class PriceHelperSpec extends ObjectBehavior } function it_returns_variant_price_for_channel_given_in_context( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, ChannelInterface $channel, ProductVariantInterface $productVariant, - ProductVariantPricesCalculatorInterface $productVariantPriceCalculator, ): void { $context = ['channel' => $channel]; - $productVariantPriceCalculator->calculate($productVariant, $context)->willReturn(1000); + $productVariantPricesCalculator->calculate($productVariant, $context)->willReturn(1000); $this->getPrice($productVariant, $context)->shouldReturn(1000); } function it_throws_invalid_argument_exception_when_channel_key_is_not_present_in_context( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, ProductVariantInterface $productVariant, - ProductVariantPricesCalculatorInterface $productVariantPriceCalculator, ): void { $context = ['lennahc' => '']; $this->shouldThrow(\InvalidArgumentException::class)->during('getPrice', [$productVariant, $context]); - $productVariantPriceCalculator->calculate($productVariant, $context)->shouldNotBeCalled(); + $productVariantPricesCalculator->calculate($productVariant, $context)->shouldNotBeCalled(); } function it_returns_variant_original_price_for_channel_given_in_context( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, ChannelInterface $channel, ProductVariantInterface $productVariant, - ProductVariantPricesCalculatorInterface $productVariantPriceCalculator, ): void { $context = ['channel' => $channel]; - $productVariantPriceCalculator->calculateOriginal($productVariant, $context)->willReturn(1000); + $productVariantPricesCalculator->calculateOriginal($productVariant, $context)->willReturn(1000); $this->getOriginalPrice($productVariant, $context)->shouldReturn(1000); } function it_throws_invalid_argument_exception_when_channel_key_is_not_present_in_context_when_getting_original_price( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, ProductVariantInterface $productVariant, - ProductVariantPricesCalculatorInterface $productVariantPriceCalculator, ): void { $context = ['lennahc' => '']; $this->shouldThrow(\InvalidArgumentException::class)->during('getOriginalPrice', [$productVariant, $context]); - $productVariantPriceCalculator->calculateOriginal($productVariant, $context)->shouldNotBeCalled(); + $productVariantPricesCalculator->calculateOriginal($productVariant, $context)->shouldNotBeCalled(); } function it_returns_true_if_variant_is_discounted_for_channel_given_in_context( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, ChannelInterface $channel, ProductVariantInterface $productVariant, - ProductVariantPricesCalculatorInterface $productVariantPriceCalculator, ): void { $context = ['channel' => $channel]; - $productVariantPriceCalculator->calculate($productVariant, $context)->willReturn(950); - $productVariantPriceCalculator->calculateOriginal($productVariant, $context)->willReturn(1000); + $productVariantPricesCalculator->calculate($productVariant, $context)->willReturn(950); + $productVariantPricesCalculator->calculateOriginal($productVariant, $context)->willReturn(1000); $this->hasDiscount($productVariant, $context)->shouldReturn(true); } function it_returns_false_if_variant_is_not_discounted_for_channel_given_in_context( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, ChannelInterface $channel, ProductVariantInterface $productVariant, - ProductVariantPricesCalculatorInterface $productVariantPriceCalculator, ): void { $context = ['channel' => $channel]; - $productVariantPriceCalculator->calculate($productVariant, $context)->willReturn(1000); - $productVariantPriceCalculator->calculateOriginal($productVariant, $context)->willReturn(1000); + $productVariantPricesCalculator->calculate($productVariant, $context)->willReturn(1000); + $productVariantPricesCalculator->calculateOriginal($productVariant, $context)->willReturn(1000); $this->hasDiscount($productVariant, $context)->shouldReturn(false); } function it_throws_invalid_argument_exception_when_channel_key_is_not_present_in_context_when_checking_if_variant_is_discounted( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, ProductVariantInterface $productVariant, - ProductVariantPricesCalculatorInterface $productVariantPriceCalculator, ): void { $context = ['lennahc' => '']; $this->shouldThrow(\InvalidArgumentException::class)->during('hasDiscount', [$productVariant, $context]); - $productVariantPriceCalculator->calculate($productVariant, $context)->shouldNotBeCalled(); - $productVariantPriceCalculator->calculateOriginal($productVariant, $context)->shouldNotBeCalled(); + $productVariantPricesCalculator->calculate($productVariant, $context)->shouldNotBeCalled(); + $productVariantPricesCalculator->calculateOriginal($productVariant, $context)->shouldNotBeCalled(); } function it_has_name(): void { $this->getName()->shouldReturn('sylius_calculate_price'); } + + function it_throws_an_exception_if_channel_is_not_provided_when_getting_lowest_price(ProductVariantInterface $productVariant): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('getLowestPriceBeforeDiscount', [$productVariant, []]) + ; + } + + function it_returns_lowest_price_before_discount( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, + ProductVariantInterface $productVariant, + ChannelInterface $channel, + ): void { + $productVariantPricesCalculator + ->calculateLowestPriceBeforeDiscount($productVariant, ['channel' => $channel]) + ->willReturn(1000) + ; + + $this + ->getLowestPriceBeforeDiscount($productVariant, ['channel' => $channel]) + ->shouldReturn(1000) + ; + } + + function it_returns_null_when_lowest_price_before_discount_is_unavailable( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, + ProductVariantInterface $productVariant, + ChannelInterface $channel, + ): void { + $productVariantPricesCalculator + ->calculateLowestPriceBeforeDiscount($productVariant, ['channel' => $channel]) + ->willReturn(null) + ; + + $this + ->getLowestPriceBeforeDiscount($productVariant, ['channel' => $channel]) + ->shouldReturn(null) + ; + } + + function it_throws_an_exception_if_channel_is_not_provided_when_checking_if_lowest_price_is_available(ProductVariantInterface $productVariant): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('hasLowestPriceBeforeDiscount', [$productVariant, []]) + ; + } + + function it_returns_true_if_lowest_price_before_discount_is_available( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, + ProductVariantInterface $productVariant, + ChannelInterface $channel, + ): void { + $productVariantPricesCalculator + ->calculateLowestPriceBeforeDiscount($productVariant, ['channel' => $channel]) + ->willReturn(1000) + ; + + $this + ->hasLowestPriceBeforeDiscount($productVariant, ['channel' => $channel]) + ->shouldReturn(true) + ; + } + + function it_returns_false_if_lowest_price_before_discount_is_unavailable( + ProductVariantPricesCalculatorInterface $productVariantPricesCalculator, + ProductVariantInterface $productVariant, + ChannelInterface $channel, + ): void { + $productVariantPricesCalculator + ->calculateLowestPriceBeforeDiscount($productVariant, ['channel' => $channel]) + ->willReturn(null) + ; + + $this + ->hasLowestPriceBeforeDiscount($productVariant, ['channel' => $channel]) + ->shouldReturn(false) + ; + } } diff --git a/src/Sylius/Bundle/CoreBundle/spec/Theme/ChannelBasedThemeContextSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Theme/ChannelBasedThemeContextSpec.php index 127c004756..155bb7d883 100644 --- a/src/Sylius/Bundle/CoreBundle/spec/Theme/ChannelBasedThemeContextSpec.php +++ b/src/Sylius/Bundle/CoreBundle/spec/Theme/ChannelBasedThemeContextSpec.php @@ -14,6 +14,7 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\CoreBundle\Theme; use PhpSpec\ObjectBehavior; +use Prophecy\Argument; use Sylius\Bundle\ThemeBundle\Context\ThemeContextInterface; use Sylius\Bundle\ThemeBundle\Model\ThemeInterface; use Sylius\Bundle\ThemeBundle\Repository\ThemeRepositoryInterface; @@ -53,7 +54,40 @@ final class ChannelBasedThemeContextSpec extends ObjectBehavior ): void { $channelContext->getChannel()->willReturn($channel); $channel->getThemeName()->willReturn(null); - $themeRepository->findOneByName(null)->willReturn(null); + $themeRepository->findOneByName(Argument::any())->shouldNotBeCalled(); + + $this->getTheme()->shouldReturn(null); + } + + function it_returns_previously_found_theme( + ChannelContextInterface $channelContext, + ThemeRepositoryInterface $themeRepository, + ThemeInterface $theme, + ): void { + $object = $this->object->getWrappedObject(); + $objectReflection = new \ReflectionObject($object); + $property = $objectReflection->getProperty('theme'); + $property->setAccessible(true); + $property->setValue($object, $theme->getWrappedObject()); + + $channelContext->getChannel()->shouldNotBeCalled(); + $themeRepository->findOneByName(Argument::any())->shouldNotBeCalled(); + + $this->getTheme()->shouldReturn($theme); + } + + function it_returns_null_if_the_theme_was_not_found_previously( + ChannelContextInterface $channelContext, + ThemeRepositoryInterface $themeRepository, + ): void { + $object = $this->object->getWrappedObject(); + $objectReflection = new \ReflectionObject($object); + $property = $objectReflection->getProperty('theme'); + $property->setAccessible(true); + $property->setValue($object, null); + + $channelContext->getChannel()->shouldNotBeCalled(); + $themeRepository->findOneByName(Argument::any())->shouldNotBeCalled(); $this->getTheme()->shouldReturn(null); } diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ChannelCodeCollectionValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ChannelCodeCollectionValidatorSpec.php new file mode 100644 index 0000000000..cc9f9fa362 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ChannelCodeCollectionValidatorSpec.php @@ -0,0 +1,219 @@ +beConstructedWith($channelRepository, $propertyAccessor); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_channel_code_collection( + Constraint $constraint, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [[], $constraint]) + ; + } + + function it_throws_an_exception_if_value_is_not_an_array(): void + { + $this + ->shouldThrow(UnexpectedValueException::class) + ->during('validate', ['', new ChannelCodeCollection()]) + ; + } + + function it_throws_exception_when_validating_using_local_channels_and_object_does_not_implement_channels_aware_interface( + ExecutionContextInterface $context, + ): void { + $context->getObject()->willReturn(new \stdClass()); + + $this + ->shouldThrow(\LogicException::class) + ->during('validate', [[], new ChannelCodeCollection(['validateAgainstAllChannels' => false, 'channelAwarePropertyPath' => 'promotion'])]) + ; + } + + function it_retrieves_an_object_from_value_and_validates_collections_for_local_channels( + ChannelRepositoryInterface $channelRepository, + ChannelInterface $channelMobile, + ChannelInterface $channelWeb, + ChannelsAwareInterface $channelsAware, + ContextualValidatorInterface $contextualValidator, + ExecutionContextInterface $context, + Form $form, + PropertyAccessorInterface $propertyAccessor, + ValidatorInterface $validator, + ): void { + $channelWeb->getCode()->willReturn('WEB'); + $channelMobile->getCode()->willReturn('MOBILE'); + + $channelsAware->getChannels()->willReturn(new ArrayCollection([ + $channelMobile->getWrappedObject(), + $channelWeb->getWrappedObject(), + ])); + + $context->getObject()->willReturn($form); + $propertyAccessor->getValue($form, 'shippingMethod')->willReturn($channelsAware); + + $constraints = [new NotBlank(), new Type('numeric')]; + $groups = ['Default', 'test_group']; + $value = ['one', 'two']; + + $collection = new Collection( + [ + 'WEB' => $constraints, + 'MOBILE' => $constraints, + ], + $groups, + ); + + $channelRepository->findAll()->shouldNotBeCalled(); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator->validate($value, $collection, $groups)->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($value, new ChannelCodeCollection([ + 'constraints' => $constraints, + 'groups' => $groups, + 'channelAwarePropertyPath' => 'shippingMethod', + ])); + } + + function it_validates_collections_for_local_channels( + ChannelRepositoryInterface $channelRepository, + ExecutionContextInterface $context, + ChannelInterface $channelWeb, + ChannelInterface $channelMobile, + PropertyAccessorInterface $propertyAccessor, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ChannelsAwareInterface $channelsAware, + ): void { + $channelWeb->getCode()->willReturn('WEB'); + $channelMobile->getCode()->willReturn('MOBILE'); + + $channelsAware->getChannels()->willReturn(new ArrayCollection([ + $channelMobile->getWrappedObject(), + $channelWeb->getWrappedObject(), + ])); + + $context->getObject()->willReturn($channelsAware); + $propertyAccessor->getValue($channelsAware, 'promotion')->willReturn($channelsAware); + + $constraints = [new NotBlank(), new Type('numeric')]; + $groups = ['Default', 'test_group']; + $value = ['one', 'two']; + + $collection = new Collection( + [ + 'WEB' => $constraints, + 'MOBILE' => $constraints, + ], + $groups, + ); + + $channelRepository->findAll()->shouldNotBeCalled(); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator->validate($value, $collection, $groups)->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($value, new ChannelCodeCollection([ + 'constraints' => $constraints, + 'groups' => $groups, + 'channelAwarePropertyPath' => 'promotion', + ])); + } + + function it_does_nothing_when_local_collection_if_channels_is_empty( + ChannelRepositoryInterface $channelRepository, + ExecutionContextInterface $context, + ChannelsAwareInterface $channelsAware, + PropertyAccessorInterface $propertyAccessor, + ): void { + $channelsAware->getChannels()->willReturn(new ArrayCollection()); + + $context->getObject()->willReturn($channelsAware); + $propertyAccessor->getValue($channelsAware, 'promotion')->willReturn($channelsAware); + + $channelRepository->findAll()->shouldNotBeCalled(); + + $context->getValidator()->shouldNotBeCalled(); + + $this->validate([], new ChannelCodeCollection(['validateAgainstAllChannels' => false, 'channelAwarePropertyPath' => 'promotion'])); + } + + function it_validates_collections_for_all_channels( + ChannelRepositoryInterface $channelRepository, + ExecutionContextInterface $context, + ChannelInterface $channelWeb, + ChannelInterface $channelMobile, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ): void { + $channelWeb->getCode()->willReturn('WEB'); + $channelMobile->getCode()->willReturn('MOBILE'); + $channelRepository->findAll()->willReturn([$channelWeb, $channelMobile]); + + $constraints = [new NotBlank(), new Type('numeric')]; + $groups = ['Default', 'test_group']; + $value = ['one', 'two']; + + $collection = new Collection( + [ + 'WEB' => $constraints, + 'MOBILE' => $constraints, + ], + $groups, + ); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator->validate($value, $collection, $groups)->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($value, new ChannelCodeCollection([ + 'constraints' => $constraints, + 'groups' => $groups, + 'validateAgainstAllChannels' => true, + ])); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/CountryCodeExistsValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/CountryCodeExistsValidatorSpec.php new file mode 100644 index 0000000000..eb3236823a --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/CountryCodeExistsValidatorSpec.php @@ -0,0 +1,82 @@ +beConstructedWith($countryRepository); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_country_code_exists( + Constraint $constraint, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', ['country_code', $constraint]) + ; + } + + function it_does_nothing_if_value_is_empty( + RepositoryInterface $countryRepository, + ExecutionContextInterface $context, + ): void { + $this->validate('', new CountryCodeExists()); + + $context->buildViolation(self::MESSAGE)->shouldNotHaveBeenCalled(); + $countryRepository->findOneBy(Argument::any())->shouldNotHaveBeenCalled(); + } + + function it_does_nothing_if_country_with_given_code_exists( + RepositoryInterface $countryRepository, + ExecutionContextInterface $context, + CountryInterface $country, + ): void { + $countryRepository->findOneBy(['code' => 'country_code'])->willReturn($country); + $this->validate('country_code', new CountryCodeExists()); + + $context->buildViolation(self::MESSAGE)->shouldNotHaveBeenCalled(); + } + + function it_adds_a_violation_if_country_with_given_code_does_not_exist( + RepositoryInterface $countryRepository, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ): void { + $countryRepository->findOneBy(['code' => 'country_code'])->willReturn(null); + + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + $constraintViolationBuilder->atPath(Argument::any())->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->willReturn($constraintViolationBuilder); + + $context->buildViolation(self::MESSAGE)->shouldBeCalled()->willReturn($constraintViolationBuilder); + + $this->validate('country_code', new CountryCodeExists()); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/CustomerGroupCodeExistsValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/CustomerGroupCodeExistsValidatorSpec.php new file mode 100644 index 0000000000..aaa781edad --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/CustomerGroupCodeExistsValidatorSpec.php @@ -0,0 +1,82 @@ +beConstructedWith($customerGroupRepository); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_customer_group_code_exists( + Constraint $constraint, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', ['customer_group_code', $constraint]) + ; + } + + function it_does_nothing_if_value_is_empty( + CustomerGroupRepositoryInterface $customerGroupRepository, + ExecutionContextInterface $context, + ): void { + $this->validate('', new CustomerGroupCodeExists()); + + $context->buildViolation(self::MESSAGE)->shouldNotHaveBeenCalled(); + $customerGroupRepository->findOneBy(Argument::any())->shouldNotHaveBeenCalled(); + } + + function it_does_nothing_if_customer_group_with_given_code_exists( + CustomerGroupRepositoryInterface $customerGroupRepository, + ExecutionContextInterface $context, + CustomerGroupInterface $customerGroup, + ): void { + $customerGroupRepository->findOneBy(['code' => 'customer_group_code'])->willReturn($customerGroup); + $this->validate('customer_group_code', new CustomerGroupCodeExists()); + + $context->buildViolation(self::MESSAGE)->shouldNotHaveBeenCalled(); + } + + function it_adds_a_violation_if_customer_group_with_given_code_does_not_exist( + CustomerGroupRepositoryInterface $customerGroupRepository, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ): void { + $customerGroupRepository->findOneBy(['code' => 'customer_group_code'])->willReturn(null); + + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + $constraintViolationBuilder->atPath(Argument::any())->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->willReturn($constraintViolationBuilder); + + $context->buildViolation(self::MESSAGE)->shouldBeCalled()->willReturn($constraintViolationBuilder); + + $this->validate('customer_group_code', new CustomerGroupCodeExists()); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ExistingChannelCodeValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ExistingChannelCodeValidatorSpec.php new file mode 100644 index 0000000000..909ab57fb7 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ExistingChannelCodeValidatorSpec.php @@ -0,0 +1,116 @@ +beConstructedWith($channelRepository); + + $this->initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldHaveType(ConstraintValidator::class); + } + + function it_throws_an_exception_if_value_is_not_a_string_or_null( + ChannelRepositoryInterface $channelRepository, + ExecutionContextInterface $context, + ): void { + $channelRepository->findOneByCode(Argument::any())->shouldNotBeCalled(); + + $context->buildViolation((new ExistingChannelCode())->message)->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new \stdClass(), new ExistingChannelCode()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_existing_channel_code( + ChannelRepositoryInterface $channelRepository, + ExecutionContextInterface $context, + Constraint $constraint, + ): void { + $channelRepository->findOneByCode(Argument::any())->shouldNotBeCalled(); + + $context->buildViolation((new ExistingChannelCode())->message)->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', ['channel_code', $constraint]) + ; + } + + function it_does_nothing_if_value_is_null( + ChannelRepositoryInterface $channelRepository, + ExecutionContextInterface $context, + ): void { + $channelRepository->findOneByCode(Argument::any())->shouldNotBeCalled(); + + $context->buildViolation((new ExistingChannelCode())->message)->shouldNotBeCalled(); + + $this->validate(null, new ExistingChannelCode()); + } + + function it_does_nothing_if_value_is_empty( + ChannelRepositoryInterface $channelRepository, + ExecutionContextInterface $context, + ): void { + $channelRepository->findOneByCode(Argument::any())->shouldNotBeCalled(); + + $context->buildViolation((new ExistingChannelCode())->message)->shouldNotBeCalled(); + + $this->validate('', new ExistingChannelCode()); + } + + function it_adds_a_violation_if_channel_with_given_code_does_not_exist( + ChannelRepositoryInterface $channelRepository, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ): void { + $channelRepository->findOneByCode('channel_code')->willReturn(null); + + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + $constraintViolationBuilder->setParameter(Argument::cetera())->willReturn($constraintViolationBuilder); + + $context->buildViolation((new ExistingChannelCode())->message)->shouldBeCalled()->willReturn($constraintViolationBuilder); + + $this->validate('channel_code', new ExistingChannelCode()); + } + + function it_does_not_add_violation_if_channel_with_given_code_exists( + ChannelRepositoryInterface $channelRepository, + ExecutionContextInterface $context, + ): void { + $channelRepository->findOneByCode('channel_code')->willReturn(new Channel()); + + $context->buildViolation((new ExistingChannelCode())->message)->shouldNotBeCalled(); + + $this->validate('channel_code', new ExistingChannelCode()); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/HasAllPricesDefinedValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/HasAllPricesDefinedValidatorSpec.php new file mode 100644 index 0000000000..dc717bbb4b --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/HasAllPricesDefinedValidatorSpec.php @@ -0,0 +1,160 @@ +initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldImplement(ConstraintValidatorInterface::class); + } + + function it_throws_an_exception_if_value_is_not_a_product_variant( + ExecutionContextInterface $context, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new \stdClass(), new HasAllPricesDefined()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_a_has_all_prices_defined_constraint( + ExecutionContextInterface $context, + Constraint $constraint, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new \stdClass(), $constraint]) + ; + } + + function it_does_nothing_if_product_variant_has_no_product( + ExecutionContextInterface $context, + ProductVariantInterface $productVariant, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $productVariant->getProduct()->willReturn(null); + + $this->validate($productVariant, new HasAllPricesDefined()); + } + + function it_adds_a_violation_if_product_variant_does_not_have_any_channel_pricing( + ExecutionContextInterface $context, + ChannelInterface $firstChannel, + ChannelInterface $secondChannel, + ChannelPricingInterface $channelPricing, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ProductInterface $product, + ProductVariantInterface $productVariant, + ): void { + $productVariant->getProduct()->willReturn($product); + $productVariant->getChannelPricingForChannel($firstChannel)->willReturn($channelPricing); + $productVariant->getChannelPricingForChannel($secondChannel)->willReturn(null); + + $channelPricing->getPrice()->willReturn(1000); + + $product + ->getChannels() + ->willReturn(new ArrayCollection([$firstChannel->getWrappedObject(), $secondChannel->getWrappedObject()])) + ; + + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + $constraintViolationBuilder->atPath(Argument::any())->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->willReturn($constraintViolationBuilder); + + $context->buildViolation((new HasAllPricesDefined())->message)->willReturn($constraintViolationBuilder); + + $this->validate($productVariant, new HasAllPricesDefined()); + } + + function it_adds_a_violation_if_product_variant_does_not_have_any_channel_pricing_price_defined( + ExecutionContextInterface $context, + ChannelInterface $firstChannel, + ChannelInterface $secondChannel, + ChannelPricingInterface $firstChannelPricing, + ChannelPricingInterface $secondChannelPricing, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ProductInterface $product, + ProductVariantInterface $productVariant, + ): void { + $productVariant->getProduct()->willReturn($product); + $productVariant->getChannelPricingForChannel($firstChannel)->willReturn($firstChannelPricing); + $productVariant->getChannelPricingForChannel($secondChannel)->willReturn($secondChannelPricing); + + $firstChannelPricing->getPrice()->willReturn(1000); + $secondChannelPricing->getPrice()->willReturn(null); + + $product + ->getChannels() + ->willReturn(new ArrayCollection([$firstChannel->getWrappedObject(), $secondChannel->getWrappedObject()])) + ; + + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + $constraintViolationBuilder->atPath(Argument::any())->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->willReturn($constraintViolationBuilder); + + $context->buildViolation((new HasAllPricesDefined())->message)->willReturn($constraintViolationBuilder); + + $this->validate($productVariant, new HasAllPricesDefined()); + } + + function it_does_not_add_a_violation_if_product_variant_has_all_channel_pricing_prices_defined( + ExecutionContextInterface $context, + ChannelInterface $firstChannel, + ChannelInterface $secondChannel, + ChannelPricingInterface $firstChannelPricing, + ChannelPricingInterface $secondChannelPricing, + ProductInterface $product, + ProductVariantInterface $productVariant, + ): void { + $productVariant->getProduct()->willReturn($product); + $productVariant->getChannelPricingForChannel($firstChannel)->willReturn($firstChannelPricing); + $productVariant->getChannelPricingForChannel($secondChannel)->willReturn($secondChannelPricing); + + $firstChannelPricing->getPrice()->willReturn(1000); + $secondChannelPricing->getPrice()->willReturn(2000); + + $product + ->getChannels() + ->willReturn(new ArrayCollection([$firstChannel->getWrappedObject(), $secondChannel->getWrappedObject()])) + ; + + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate($productVariant, new HasAllPricesDefined()); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/HasEnabledEntityValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/HasEnabledEntityValidatorSpec.php index 4bc207d52b..bb7cd68be9 100644 --- a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/HasEnabledEntityValidatorSpec.php +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/HasEnabledEntityValidatorSpec.php @@ -14,18 +14,232 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\CoreBundle\Validator\Constraints; use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectRepository; use PhpSpec\ObjectBehavior; +use Prophecy\Argument; +use Sylius\Bundle\CoreBundle\Validator\Constraints\HasEnabledEntity; +use Sylius\Component\Resource\Model\ToggleableInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; final class HasEnabledEntityValidatorSpec extends ObjectBehavior { - public function let(ManagerRegistry $registry) - { - $this->beConstructedWith($registry); + public function let( + ManagerRegistry $registry, + PropertyAccessorInterface $accessor, + ExecutionContextInterface $context, + ): void { + $this->beConstructedWith($registry, $accessor); + + $this->initialize($context); } - public function it_is_a_constraint_validator() + public function it_is_a_constraint_validator(): void { $this->shouldHaveType(ConstraintValidator::class); } + + public function it_throws_exception_when_constraint_is_not_a_has_enabled_entity( + ManagerRegistry $registry, + PropertyAccessorInterface $accessor, + ExecutionContextInterface $context, + Constraint $constraint, + ): void { + $accessor->getValue(Argument::cetera())->shouldNotBeCalled(); + $registry->getManager(Argument::any())->shouldNotBeCalled(); + $context->buildViolation(Argument::cetera())->shouldNotBeCalled(); + + $this->shouldThrow(\InvalidArgumentException::class)->during('validate', [null, $constraint]); + } + + public function it_throws_exception_when_value_is_not_an_object( + ManagerRegistry $registry, + PropertyAccessorInterface $accessor, + ExecutionContextInterface $context, + ): void { + $constraint = new HasEnabledEntity(); + $accessor->getValue(Argument::cetera())->shouldNotBeCalled(); + $registry->getManager(Argument::any())->shouldNotBeCalled(); + $context->buildViolation(Argument::cetera())->shouldNotBeCalled(); + + $this->shouldThrow(\InvalidArgumentException::class)->during('validate', [null, $constraint]); + } + + public function it_does_nothing_when_entity_is_enabled( + ManagerRegistry $registry, + PropertyAccessorInterface $accessor, + ExecutionContextInterface $context, + ToggleableInterface $entity, + ): void { + $constraint = new HasEnabledEntity(); + $constraint->enabledPath = 'enabled'; + + $accessor->getValue($entity, 'enabled')->willReturn(true); + $registry->getManager($constraint->objectManager)->shouldNotBeCalled(); + $context->buildViolation(Argument::cetera())->shouldNotBeCalled(); + + $this->validate($entity, $constraint); + } + + public function it_throws_exception_when_manager_specified_by_constraint_is_not_found( + ManagerRegistry $registry, + PropertyAccessorInterface $accessor, + ExecutionContextInterface $context, + ToggleableInterface $entity, + ): void { + $constraint = new HasEnabledEntity(); + $constraint->enabledPath = 'enabled'; + $constraint->objectManager = 'custom'; + + $accessor->getValue($entity, 'enabled')->willReturn(false); + $registry->getManager($constraint->objectManager)->willReturn(null); + $context->buildViolation(Argument::cetera())->shouldNotBeCalled(); + + $this->shouldThrow(ConstraintDefinitionException::class)->during('validate', [$entity, $constraint]); + } + + public function it_throws_exception_when_no_manager_is_specified_by_constraint_and_no_manager_can_be_found_for_value( + ManagerRegistry $registry, + PropertyAccessorInterface $accessor, + ExecutionContextInterface $context, + ToggleableInterface $entity, + ): void { + $constraint = new HasEnabledEntity(); + $constraint->enabledPath = 'enabled'; + $constraint->objectManager = null; + + $accessor->getValue($entity, 'enabled')->willReturn(false); + $registry->getManager(Argument::any())->shouldNotBeCalled(); + $context->buildViolation(Argument::cetera())->shouldNotBeCalled(); + + $registry->getManagerForClass($entity->getWrappedObject()::class)->willReturn(null); + + $this->shouldThrow(ConstraintDefinitionException::class)->during('validate', [$entity, $constraint]); + } + + public function it_throws_exception_when_enabled_field_is_neither_a_mapped_field_or_association( + ManagerRegistry $registry, + PropertyAccessorInterface $accessor, + ExecutionContextInterface $context, + ObjectManager $manager, + ClassMetadata $metadata, + ToggleableInterface $entity, + ): void { + $constraint = new HasEnabledEntity(); + $constraint->enabledPath = 'enabled'; + $constraint->objectManager = 'custom'; + + $accessor->getValue($entity, 'enabled')->willReturn(false); + $registry->getManager('custom')->willReturn($manager); + $manager->getClassMetadata($entity->getWrappedObject()::class)->willReturn($metadata); + + $metadata->hasField('enabled')->willReturn(false); + $metadata->hasAssociation('enabled')->willReturn(false); + + $context->buildViolation(Argument::cetera())->shouldNotBeCalled(); + + $this->shouldThrow(ConstraintDefinitionException::class)->during('validate', [$entity, $constraint]); + } + + public function it_does_nothing_when_passed_value_is_not_the_last_enabled_entity( + ManagerRegistry $registry, + PropertyAccessorInterface $accessor, + ExecutionContextInterface $context, + ObjectManager $manager, + ClassMetadata $metadata, + ObjectRepository $repository, + ToggleableInterface $entity, + ToggleableInterface $anotherEntity, + ): void { + $constraint = new HasEnabledEntity(); + $constraint->enabledPath = 'enabled'; + $constraint->objectManager = 'custom'; + + $accessor->getValue($entity, 'enabled')->willReturn(false); + $registry->getManager('custom')->willReturn($manager); + $manager->getClassMetadata($entity->getWrappedObject()::class)->willReturn($metadata); + $metadata->hasField('enabled')->willReturn(true); + + $manager->getRepository($entity->getWrappedObject()::class)->willReturn($repository); + + $repository->findBy(['enabled' => true])->willReturn([ + $entity->getWrappedObject(), + $anotherEntity->getWrappedObject(), + ]); + + $context->buildViolation(Argument::cetera())->shouldNotBeCalled(); + + $this->validate($entity, $constraint); + } + + public function it_adds_violation_if_passed_value_is_the_only_enabled_entity( + ManagerRegistry $registry, + PropertyAccessorInterface $accessor, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $violationBuilder, + ObjectManager $manager, + ClassMetadata $metadata, + ObjectRepository $repository, + ToggleableInterface $entity, + ): void { + $constraint = new HasEnabledEntity(); + $constraint->enabledPath = 'enabled'; + $constraint->objectManager = 'custom'; + + $accessor->getValue($entity, 'enabled')->willReturn(false); + $registry->getManager('custom')->willReturn($manager); + $manager->getClassMetadata($entity->getWrappedObject()::class)->willReturn($metadata); + $metadata->hasField('enabled')->willReturn(true); + + $manager->getRepository($entity->getWrappedObject()::class)->willReturn($repository); + + $repository->findBy(['enabled' => true])->willReturn([ + $entity->getWrappedObject(), + ]); + + $context->buildViolation($constraint->message)->willReturn($violationBuilder); + $violationBuilder->atPath('enabled')->shouldBeCalled()->willReturn($violationBuilder); + $violationBuilder->addViolation()->shouldBeCalled(); + + $this->validate($entity, $constraint); + } + + public function it_adds_violation_at_custom_path_if_passed_value_is_the_only_enabled_entity( + ManagerRegistry $registry, + PropertyAccessorInterface $accessor, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $violationBuilder, + ObjectManager $manager, + ClassMetadata $metadata, + ObjectRepository $repository, + ToggleableInterface $entity, + ): void { + $constraint = new HasEnabledEntity(); + $constraint->enabledPath = 'enabled'; + $constraint->objectManager = 'custom'; + $constraint->errorPath = 'customPath'; + + $accessor->getValue($entity, 'enabled')->willReturn(false); + $registry->getManager('custom')->willReturn($manager); + $manager->getClassMetadata($entity->getWrappedObject()::class)->willReturn($metadata); + $metadata->hasField('enabled')->willReturn(true); + + $manager->getRepository($entity->getWrappedObject()::class)->willReturn($repository); + + $repository->findBy(['enabled' => true])->willReturn([ + $entity->getWrappedObject(), + ]); + + $context->buildViolation($constraint->message)->willReturn($violationBuilder); + $violationBuilder->atPath('customPath')->shouldBeCalled()->willReturn($violationBuilder); + $violationBuilder->addViolation()->shouldBeCalled(); + + $this->validate($entity, $constraint); + } } diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductCodeExistsValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductCodeExistsValidatorSpec.php new file mode 100644 index 0000000000..eef7a9419c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductCodeExistsValidatorSpec.php @@ -0,0 +1,82 @@ +beConstructedWith($productRepository); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_product_code_exists( + Constraint $constraint, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', ['product_code', $constraint]) + ; + } + + function it_does_nothing_if_value_is_empty( + ProductRepositoryInterface $productRepository, + ExecutionContextInterface $context, + ): void { + $this->validate('', new ProductCodeExists()); + + $context->buildViolation(self::MESSAGE)->shouldNotHaveBeenCalled(); + $productRepository->findOneBy(Argument::any())->shouldNotHaveBeenCalled(); + } + + function it_does_nothing_if_product_with_given_code_exists( + ProductRepositoryInterface $productRepository, + ExecutionContextInterface $context, + ProductInterface $product, + ): void { + $productRepository->findOneByCode('product_code')->willReturn($product); + $this->validate('product_code', new ProductCodeExists()); + + $context->buildViolation(self::MESSAGE)->shouldNotHaveBeenCalled(); + } + + function it_adds_a_violation_if_product_with_given_code_does_not_exist( + ProductRepositoryInterface $productRepository, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ): void { + $productRepository->findOneByCode('product_code')->willReturn(null); + + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + $constraintViolationBuilder->atPath(Argument::any())->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->willReturn($constraintViolationBuilder); + + $context->buildViolation(self::MESSAGE)->shouldBeCalled()->willReturn($constraintViolationBuilder); + + $this->validate('product_code', new ProductCodeExists()); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductImageVariantsBelongToOwnerValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductImageVariantsBelongToOwnerValidatorSpec.php new file mode 100644 index 0000000000..4e73d3130c --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductImageVariantsBelongToOwnerValidatorSpec.php @@ -0,0 +1,109 @@ +initialize($executionContext); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldImplement(ConstraintValidatorInterface::class); + } + + function it_throws_an_exception_if_value_is_not_product_image(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new \stdClass(), new ProductImageVariantsBelongToOwner()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_product_image_variants_belong_to_owner( + Constraint $constraint, + ProductImageInterface $image, + ): void { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [$image, $constraint]) + ; + } + + function it_adds_a_violation_if_any_variant_does_not_belong_to_a_product_which_is_an_owner( + ExecutionContextInterface $executionContext, + ProductImageInterface $image, + ProductInterface $product, + ProductVariantInterface $variant, + ): void { + $image->getOwner()->willReturn($product); + $image->getProductVariants()->willReturn(new ArrayCollection([$variant->getWrappedObject()])); + + $product->getCode()->willReturn('MUG'); + $product->hasVariant($variant)->willReturn(false); + + $variant->getCode()->willReturn('GREEN_SHIRT'); + + $executionContext + ->addViolation( + 'sylius.product_image.product_variant.not_belong_to_owner', + ['%productVariantCode%' => 'GREEN_SHIRT', '%ownerCode%' => 'MUG'], + ) + ->shouldBeCalled() + ; + + $this->validate($image, new ProductImageVariantsBelongToOwner()); + } + + function it_does_nothing_if_all_variants_belong_to_a_product_which_is_an_owner( + ExecutionContextInterface $executionContext, + ProductImageInterface $image, + ProductInterface $product, + ProductVariantInterface $firstVariant, + ProductVariantInterface $secondVariant, + ): void { + $image->getOwner()->willReturn($product); + $image->getProductVariants()->willReturn(new ArrayCollection([ + $firstVariant->getWrappedObject(), + $secondVariant->getWrappedObject(), + ])); + + $product->getCode()->willReturn('MUG'); + $product->hasVariant($firstVariant)->willReturn(true); + $product->hasVariant($secondVariant)->willReturn(true); + + $executionContext + ->addViolation( + 'sylius.product_image.product_variant.not_belong_to_owner', + Argument::any(), + ) + ->shouldNotBeCalled() + ; + + $this->validate($image, new ProductImageVariantsBelongToOwner()); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductVariantCodeExistsValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductVariantCodeExistsValidatorSpec.php new file mode 100644 index 0000000000..8625ab9b62 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ProductVariantCodeExistsValidatorSpec.php @@ -0,0 +1,99 @@ +beConstructedWith($productVariantRepository); + + $this->initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldHaveType(ConstraintValidator::class); + } + + function it_throws_exception_when_passed_constraint_is_not_product_variant_code_exists( + Constraint $constraint, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', ['variant_code', $constraint]) + ; + } + + function it_does_nothing_when_passed_value_is_a_null( + ProductVariantRepositoryInterface $productVariantRepository, + ExecutionContextInterface $context, + ): void { + $productVariantRepository->findOneBy(Argument::any())->shouldNotBeCalled(); + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate(null, new ProductVariantCodeExists()); + } + + function it_does_nothing_when_passed_value_is_an_empty_string( + ProductVariantRepositoryInterface $productVariantRepository, + ExecutionContextInterface $context, + ): void { + $productVariantRepository->findOneBy(Argument::any())->shouldNotBeCalled(); + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate('', new ProductVariantCodeExists()); + } + + function it_does_nothing_when_a_variant_with_passed_code_exists( + ProductVariantRepositoryInterface $productVariantRepository, + ExecutionContextInterface $context, + ProductVariantInterface $variant, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $productVariantRepository->findOneBy(['code' => 'test'])->willReturn($variant); + + $this->validate('test', new ProductVariantCodeExists()); + } + + function it_adds_violation_when_variant_with_passed_code_does_not_exist( + ProductVariantRepositoryInterface $productVariantRepository, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $violationBuilder, + ): void { + $constraint = new ProductVariantCodeExists(); + + $productVariantRepository->findOneBy(['code' => 'test'])->willReturn(null); + + $context->buildViolation($constraint->message)->willReturn($violationBuilder); + $violationBuilder->setParameter('{{ code }}', 'test')->willReturn($violationBuilder); + $violationBuilder->addViolation()->shouldBeCalled(); + + $this->validate('test', $constraint); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ResendOrderConfirmationEmailWithValidOrderStateValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ResendOrderConfirmationEmailWithValidOrderStateValidatorSpec.php new file mode 100644 index 0000000000..33bba90fb8 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ResendOrderConfirmationEmailWithValidOrderStateValidatorSpec.php @@ -0,0 +1,99 @@ +beConstructedWith($orderRepository, [OrderInterface::STATE_NEW]); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_resend_order_confirmation_email_with_valid_order_state( + Constraint $constraint, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [new ResendOrderConfirmationEmail('TOKEN'), $constraint]) + ; + } + + function it_throws_an_exception_if_value_is_not_a_resend_order_confirmation_email(): void + { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [new \stdClass(), new ResendOrderConfirmationEmailWithValidOrderState()]) + ; + } + + function it_does_nothing_if_the_state_is_valid( + RepositoryInterface $orderRepository, + ExecutionContextInterface $context, + OrderInterface $order, + ): void { + $orderRepository->findOneBy(['tokenValue' => 'TOKEN'])->willReturn($order); + $order->getState()->willReturn(OrderInterface::STATE_NEW); + + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate( + new ResendOrderConfirmationEmail('TOKEN'), + new ResendOrderConfirmationEmailWithValidOrderState(), + ); + } + + function it_does_nothing_when_order_does_not_exist( + RepositoryInterface $orderRepository, + ExecutionContextInterface $context, + ): void { + $orderRepository->findOneBy(['tokenValue' => 'TOKEN'])->willReturn(null); + + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate(new ResendOrderConfirmationEmail('TOKEN'), new ResendOrderConfirmationEmailWithValidOrderState()); + } + + function it_adds_a_violation_if_order_has_invalid_state( + RepositoryInterface $orderRepository, + OrderInterface $order, + ExecutionContextInterface $context, + ): void { + $constraint = new ResendOrderConfirmationEmailWithValidOrderState(); + + $orderRepository->findOneBy(['tokenValue' => 'TOKEN'])->willReturn($order); + $order->getState()->willReturn(OrderInterface::STATE_FULFILLED); + + $context + ->addViolation($constraint->message, ['%state%' => OrderInterface::STATE_FULFILLED]) + ->shouldBeCalled() + ; + + $this->validate( + new ResendOrderConfirmationEmail('TOKEN'), + $constraint, + ); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ResendShipmentConfirmationEmailWithValidShipmentStateValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ResendShipmentConfirmationEmailWithValidShipmentStateValidatorSpec.php new file mode 100644 index 0000000000..92451f5960 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/ResendShipmentConfirmationEmailWithValidShipmentStateValidatorSpec.php @@ -0,0 +1,84 @@ +beConstructedWith($shipmentRepository); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_resend_order_confirmation_email_with_valid_order_state( + Constraint $constraint, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [new ResendShipmentConfirmationEmail(123), $constraint]) + ; + } + + function it_throws_an_exception_if_value_is_not_resend_shipment_confirmation(): void + { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [new \stdClass(), new ResendShipmentConfirmationEmailWithValidShipmentState()]) + ; + } + + function it_does_nothing_if_the_state_is_valid( + RepositoryInterface $shipmentRepository, + ExecutionContextInterface $context, + ShipmentInterface $shipment, + ): void { + $shipmentRepository->findOneBy(['id' => 2])->willReturn($shipment); + $shipment->getState()->willReturn(ShipmentInterface::STATE_SHIPPED); + + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate( + new ResendShipmentConfirmationEmail(2), + new ResendShipmentConfirmationEmailWithValidShipmentState(), + ); + } + + function it_adds_a_violation_if_order_has_invalid_state( + RepositoryInterface $shipmentRepository, + ShipmentInterface $shipment, + ExecutionContextInterface $context, + ): void { + $constraint = new ResendShipmentConfirmationEmailWithValidShipmentState(); + $shipmentRepository->findOneBy(['id' => 2])->willReturn($shipment); + $shipment->getState()->willReturn(ShipmentInterface::STATE_CANCELLED); + + $context + ->addViolation($constraint->message, ['%state%' => ShipmentInterface::STATE_CANCELLED]) + ->shouldBeCalled() + ; + + $this->validate(new ResendShipmentConfirmationEmail(2), $constraint); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/TaxonCodeExistsValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/TaxonCodeExistsValidatorSpec.php new file mode 100644 index 0000000000..ea16559b15 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/TaxonCodeExistsValidatorSpec.php @@ -0,0 +1,82 @@ +beConstructedWith($taxonRepository); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_taxon_code_exists( + Constraint $constraint, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', ['taxon_code', $constraint]) + ; + } + + function it_does_nothing_if_value_is_empty( + TaxonRepositoryInterface $taxonRepository, + ExecutionContextInterface $context, + ): void { + $this->validate('', new TaxonCodeExists()); + + $context->buildViolation(self::MESSAGE)->shouldNotHaveBeenCalled(); + $taxonRepository->findOneBy(Argument::any())->shouldNotHaveBeenCalled(); + } + + function it_does_nothing_if_taxon_with_given_code_exists( + TaxonRepositoryInterface $taxonRepository, + ExecutionContextInterface $context, + TaxonInterface $taxon, + ): void { + $taxonRepository->findOneBy(['code' => 'taxon_code'])->willReturn($taxon); + $this->validate('taxon_code', new TaxonCodeExists()); + + $context->buildViolation(self::MESSAGE)->shouldNotHaveBeenCalled(); + } + + function it_adds_a_violation_if_taxon_with_given_code_does_not_exist( + TaxonRepositoryInterface $taxonRepository, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ): void { + $taxonRepository->findOneBy(['code' => 'taxon_code'])->willReturn(null); + + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + $constraintViolationBuilder->atPath(Argument::any())->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->willReturn($constraintViolationBuilder); + + $context->buildViolation(self::MESSAGE)->shouldBeCalled()->willReturn($constraintViolationBuilder); + + $this->validate('taxon_code', new TaxonCodeExists()); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/TranslationForExistingLocalesValidatorSpec.php b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/TranslationForExistingLocalesValidatorSpec.php new file mode 100644 index 0000000000..e4660037d5 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/spec/Validator/Constraints/TranslationForExistingLocalesValidatorSpec.php @@ -0,0 +1,148 @@ +beConstructedWith($localeRepository); + + $this->initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldImplement(ConstraintValidatorInterface::class); + } + + function it_throws_an_exception_if_value_is_not_translatable( + RepositoryInterface $localeRepository, + ExecutionContextInterface $context, + TranslatableInterface $value, + TranslationInterface $translation, + ): void { + $localeRepository->findAll()->shouldNotBeCalled(); + + $value->getTranslations()->shouldNotBeCalled(); + $translation->getLocale()->shouldNotBeCalled(); + + $context->buildViolation((new TranslationForExistingLocales())->message)->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new \stdClass(), new TranslationForExistingLocales()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_translation_for_existing_locales_constraint( + RepositoryInterface $localeRepository, + ExecutionContextInterface $context, + Constraint $constraint, + TranslatableInterface $value, + TranslationInterface $translation, + ): void { + $localeRepository->findAll()->shouldNotBeCalled(); + + $value->getTranslations()->shouldNotBeCalled(); + $translation->getLocale()->shouldNotBeCalled(); + + $context->buildViolation((new TranslationForExistingLocales())->message)->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [$value, $constraint]) + ; + } + + function it_does_nothing_if_there_is_no_locales( + RepositoryInterface $localeRepository, + ExecutionContextInterface $context, + TranslatableInterface $value, + TranslationInterface $translation, + ): void { + $localeRepository->findAll()->willReturn([]); + + $value->getTranslations()->shouldNotBeCalled(); + $translation->getLocale()->shouldNotBeCalled(); + + $context->buildViolation((new TranslationForExistingLocales())->message)->shouldNotBeCalled(); + + $this->validate($value, new TranslationForExistingLocales()); + } + + function it_adds_a_violation_if_any_translations_locale_in_the_translatable_object_is_not_included_in_the_available_locales( + RepositoryInterface $localeRepository, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + TranslatableInterface $value, + TranslationInterface $firstTranslation, + TranslationInterface $secondTranslation, + LocaleInterface $availableLocale, + ): void { + $availableLocale->getCode()->willReturn('en_US'); + $localeRepository->findAll()->willReturn([$availableLocale]); + + $value->getTranslations()->willReturn(new ArrayCollection([$firstTranslation, $secondTranslation])); + $firstTranslation->getLocale()->willReturn('en_US'); + $secondTranslation->getLocale()->willReturn('NON_EXISTING_LOCALE'); + + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + $constraintViolationBuilder->atPath(Argument::any())->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->willReturn($constraintViolationBuilder); + + $context->buildViolation((new TranslationForExistingLocales())->message)->willReturn($constraintViolationBuilder); + + $this->validate($value, new TranslationForExistingLocales()); + } + + function it_does_not_add_violation_if_the_translations_in_the_translatable_object_are_included_in_the_available_locales( + RepositoryInterface $localeRepository, + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + TranslatableInterface $value, + TranslationInterface $firstTranslation, + TranslationInterface $secondTranslation, + LocaleInterface $firstAvailableLocale, + LocaleInterface $secondAvailableLocale, + ): void { + $firstAvailableLocale->getCode()->willReturn('en_US'); + $secondAvailableLocale->getCode()->willReturn('pl_PL'); + $localeRepository->findAll()->willReturn([$firstAvailableLocale, $secondAvailableLocale]); + + $value + ->getTranslations() + ->willReturn(new ArrayCollection([$firstTranslation->getWrappedObject(), $secondTranslation->getWrappedObject()])) + ; + $firstTranslation->getLocale()->willReturn('en_US'); + $secondTranslation->getLocale()->willReturn('pl_PL'); + + $context->buildViolation((new TranslationForExistingLocales())->message)->shouldNotBeCalled(); + + $this->validate($value, new TranslationForExistingLocales()); + } +} diff --git a/src/Sylius/Bundle/CoreBundle/test/config/bundles.php b/src/Sylius/Bundle/CoreBundle/test/config/bundles.php index b8d178f6b8..0ca810acb6 100644 --- a/src/Sylius/Bundle/CoreBundle/test/config/bundles.php +++ b/src/Sylius/Bundle/CoreBundle/test/config/bundles.php @@ -19,6 +19,7 @@ return [ Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true], Sylius\Calendar\SyliusCalendarBundle::class => ['all' => true], + Sylius\Abstraction\StateMachine\SyliusStateMachineAbstractionBundle::class => ['all' => true], Sylius\Bundle\OrderBundle\SyliusOrderBundle::class => ['all' => true], Sylius\Bundle\MoneyBundle\SyliusMoneyBundle::class => ['all' => true], Sylius\Bundle\CurrencyBundle\SyliusCurrencyBundle::class => ['all' => true], diff --git a/src/Sylius/Bundle/CoreBundle/test/config/packages/config.yaml b/src/Sylius/Bundle/CoreBundle/test/config/packages/config.yaml index dca50202dc..dcdbff194e 100644 --- a/src/Sylius/Bundle/CoreBundle/test/config/packages/config.yaml +++ b/src/Sylius/Bundle/CoreBundle/test/config/packages/config.yaml @@ -15,6 +15,7 @@ framework: test: ~ mailer: dsn: 'null://null' + workflows: ~ security: firewalls: diff --git a/src/Sylius/Bundle/CoreBundle/test/config/packages/state_machine.yaml b/src/Sylius/Bundle/CoreBundle/test/config/packages/state_machine.yaml new file mode 100644 index 0000000000..4b0f3afa52 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/test/config/packages/state_machine.yaml @@ -0,0 +1,60 @@ +winzou_state_machine: + app_blog_post: + class: Sylius\Bundle\CoreBundle\Application\Model\BlogPost + property_path: state + graph: app_blog_post + state_machine_class: Sylius\Component\Resource\StateMachine\StateMachine + states: + new: ~ + published: ~ + unpublished: ~ + transitions: + post: + from: [new, unpublished] + to: published + unpost: + from: [published] + to: unpublished + app_comment: + class: Sylius\Bundle\CoreBundle\Application\Model\Comment + property_path: state + graph: app_comment + state_machine_class: Sylius\Component\Resource\StateMachine\StateMachine + states: + new: ~ + published: ~ + unpublished: ~ + transitions: + post: + from: [new, unpublished] + to: published + unpost: + from: [published] + to: unpublished + +framework: + workflows: + app_blog_post: + type: state_machine + marking_store: + type: method + property: state + supports: + - Sylius\Bundle\CoreBundle\Application\Model\BlogPost + initial_marking: new + places: + - new + - published + - unpublished + transitions: + publish: + from: [new, unpublished] + to: published + unpublish: + from: published + to: unpublished + +sylius_state_machine_abstraction: + default_adapter: 'winzou_state_machine' + graphs_to_adapters_mapping: + app_blog_post: 'symfony_workflow' diff --git a/src/Sylius/Bundle/CoreBundle/test/src/Model/BlogPost.php b/src/Sylius/Bundle/CoreBundle/test/src/Model/BlogPost.php new file mode 100644 index 0000000000..8c3f6161ea --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/test/src/Model/BlogPost.php @@ -0,0 +1,32 @@ +state; + } + + public function setState(string $state): void + { + $this->state = $state; + } +} diff --git a/src/Sylius/Bundle/CoreBundle/test/src/Model/Comment.php b/src/Sylius/Bundle/CoreBundle/test/src/Model/Comment.php new file mode 100644 index 0000000000..c5bec03097 --- /dev/null +++ b/src/Sylius/Bundle/CoreBundle/test/src/Model/Comment.php @@ -0,0 +1,32 @@ +state; + } + + public function setState(string $state): void + { + $this->state = $state; + } +} diff --git a/src/Sylius/Bundle/CurrencyBundle/Attribute/AsCurrencyContext.php b/src/Sylius/Bundle/CurrencyBundle/Attribute/AsCurrencyContext.php new file mode 100644 index 0000000000..3c94a75b1e --- /dev/null +++ b/src/Sylius/Bundle/CurrencyBundle/Attribute/AsCurrencyContext.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/CurrencyBundle/DependencyInjection/SyliusCurrencyExtension.php b/src/Sylius/Bundle/CurrencyBundle/DependencyInjection/SyliusCurrencyExtension.php index e17eb46d4c..8050867926 100644 --- a/src/Sylius/Bundle/CurrencyBundle/DependencyInjection/SyliusCurrencyExtension.php +++ b/src/Sylius/Bundle/CurrencyBundle/DependencyInjection/SyliusCurrencyExtension.php @@ -13,8 +13,10 @@ declare(strict_types=1); namespace Sylius\Bundle\CurrencyBundle\DependencyInjection; +use Sylius\Bundle\CurrencyBundle\Attribute\AsCurrencyContext; use Sylius\Bundle\ResourceBundle\DependencyInjection\Extension\AbstractResourceExtension; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -30,5 +32,17 @@ final class SyliusCurrencyExtension extends AbstractResourceExtension $this->registerResources('sylius', $config['driver'], $config['resources'], $container); $loader->load('services.xml'); + + $this->registerAutoconfiguration($container); + } + + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsCurrencyContext::class, + static function (ChildDefinition $definition, AsCurrencyContext $attribute): void { + $definition->addTag(AsCurrencyContext::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); } } diff --git a/src/Sylius/Bundle/CurrencyBundle/Doctrine/ORM/ExchangeRateRepository.php b/src/Sylius/Bundle/CurrencyBundle/Doctrine/ORM/ExchangeRateRepository.php index 7fe2de6bcb..8a53392696 100644 --- a/src/Sylius/Bundle/CurrencyBundle/Doctrine/ORM/ExchangeRateRepository.php +++ b/src/Sylius/Bundle/CurrencyBundle/Doctrine/ORM/ExchangeRateRepository.php @@ -18,6 +18,11 @@ use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Currency\Model\ExchangeRateInterface; use Sylius\Component\Currency\Repository\ExchangeRateRepositoryInterface; +/** + * @template T of ExchangeRateInterface + * + * @implements ExchangeRateRepositoryInterface + */ class ExchangeRateRepository extends EntityRepository implements ExchangeRateRepositoryInterface { /** diff --git a/src/Sylius/Bundle/CurrencyBundle/Tests/DependencyInjection/SyliusCurrencyExtensionTest.php b/src/Sylius/Bundle/CurrencyBundle/Tests/DependencyInjection/SyliusCurrencyExtensionTest.php new file mode 100644 index 0000000000..e2a7dc29e1 --- /dev/null +++ b/src/Sylius/Bundle/CurrencyBundle/Tests/DependencyInjection/SyliusCurrencyExtensionTest.php @@ -0,0 +1,48 @@ +container->setDefinition( + 'acme.currency_context_with_attribute', + (new Definition()) + ->setClass(CurrencyContextStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.currency_context_with_attribute', + AsCurrencyContext::SERVICE_TAG, + ['priority' => 15], + ); + } + + protected function getContainerExtensions(): array + { + return [new SyliusCurrencyExtension()]; + } +} diff --git a/src/Sylius/Bundle/CurrencyBundle/Tests/Stub/CurrencyContextStub.php b/src/Sylius/Bundle/CurrencyBundle/Tests/Stub/CurrencyContextStub.php new file mode 100644 index 0000000000..e821a8f165 --- /dev/null +++ b/src/Sylius/Bundle/CurrencyBundle/Tests/Stub/CurrencyContextStub.php @@ -0,0 +1,26 @@ += 2.16.0", @@ -38,12 +38,13 @@ }, "require-dev": { "doctrine/orm": "^2.13", + "matthiasnoback/symfony-dependency-injection-test": "^4.2", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", + "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0", + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4", "twig/twig": "^2.12 || ^3.3" }, "config": { @@ -53,7 +54,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { @@ -63,7 +64,8 @@ }, "autoload-dev": { "psr-4": { - "Sylius\\Bundle\\CurrencyBundle\\spec\\": "spec/" + "Sylius\\Bundle\\CurrencyBundle\\spec\\": "spec/", + "Sylius\\Bundle\\CurrencyBundle\\Tests\\": "Tests/" }, "classmap": [ "test/app/AppKernel.php" diff --git a/src/Sylius/Bundle/CustomerBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/CustomerBundle/DependencyInjection/Configuration.php index 31cba20d78..b3836b283a 100644 --- a/src/Sylius/Bundle/CustomerBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/CustomerBundle/DependencyInjection/Configuration.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\CustomerBundle\DependencyInjection; +use Sylius\Bundle\CustomerBundle\Doctrine\ORM\CustomerGroupRepository; use Sylius\Bundle\CustomerBundle\Form\Type\CustomerGroupType; use Sylius\Bundle\CustomerBundle\Form\Type\CustomerType; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; @@ -79,7 +80,7 @@ final class Configuration implements ConfigurationInterface ->scalarNode('model')->defaultValue(CustomerGroup::class)->cannotBeEmpty()->end() ->scalarNode('interface')->defaultValue(CustomerGroupInterface::class)->cannotBeEmpty()->end() ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() - ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(CustomerGroupRepository::class)->cannotBeEmpty()->end() ->scalarNode('factory')->defaultValue(Factory::class)->end() ->scalarNode('form')->defaultValue(CustomerGroupType::class)->cannotBeEmpty()->end() ->end() diff --git a/src/Sylius/Bundle/CustomerBundle/Doctrine/ORM/CustomerGroupRepository.php b/src/Sylius/Bundle/CustomerBundle/Doctrine/ORM/CustomerGroupRepository.php new file mode 100644 index 0000000000..8d9f06a65d --- /dev/null +++ b/src/Sylius/Bundle/CustomerBundle/Doctrine/ORM/CustomerGroupRepository.php @@ -0,0 +1,43 @@ + + */ +class CustomerGroupRepository extends EntityRepository implements CustomerGroupRepositoryInterface +{ + public function findByPhrase(string $phrase, ?int $limit = null): iterable + { + $expr = $this->getEntityManager()->getExpressionBuilder(); + + return $this->createQueryBuilder('o') + ->andWhere($expr->orX( + 'o.code LIKE :phrase', + 'o.name LIKE :phrase', + )) + ->setParameter('phrase', '%' . $phrase . '%') + ->addOrderBy('o.name', 'ASC') + ->setMaxResults($limit) + ->getQuery() + ->getResult() + ; + } +} diff --git a/src/Sylius/Bundle/CustomerBundle/Resources/config/serializer/Model.CustomerGroup.yml b/src/Sylius/Bundle/CustomerBundle/Resources/config/serializer/Model.CustomerGroup.yml new file mode 100644 index 0000000000..a21ddc8ae9 --- /dev/null +++ b/src/Sylius/Bundle/CustomerBundle/Resources/config/serializer/Model.CustomerGroup.yml @@ -0,0 +1,16 @@ +Sylius\Component\Customer\Model\CustomerGroup: + exclusion_policy: ALL + xml_root_name: customer-group + properties: + id: + expose: true + type: integer + groups: [Default, Detailed] + code: + expose: true + type: string + groups: [Default, Detailed, Autocomplete] + name: + expose: true + type: string + groups: [Default, Detailed, Autocomplete] diff --git a/src/Sylius/Bundle/CustomerBundle/composer.json b/src/Sylius/Bundle/CustomerBundle/composer.json index a9c1af1410..ad12215618 100644 --- a/src/Sylius/Bundle/CustomerBundle/composer.json +++ b/src/Sylius/Bundle/CustomerBundle/composer.json @@ -35,12 +35,12 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "doctrine/orm": "^2.13", "egulias/email-validator": "^3.1", - "sylius/customer": "^1.12", + "sylius/customer": "^1.13", "sylius/resource-bundle": "^1.9", - "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4.21 || ^6.4", "webmozart/assert": "^1.9" }, "conflict": { @@ -49,11 +49,11 @@ }, "require-dev": { "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", + "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0" + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4" }, "config": { "allow-plugins": { @@ -62,7 +62,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { diff --git a/src/Sylius/Bundle/InventoryBundle/Validator/Constraints/InStock.php b/src/Sylius/Bundle/InventoryBundle/Validator/Constraints/InStock.php index 7575431b6f..b8cfe6d2ba 100644 --- a/src/Sylius/Bundle/InventoryBundle/Validator/Constraints/InStock.php +++ b/src/Sylius/Bundle/InventoryBundle/Validator/Constraints/InStock.php @@ -19,6 +19,8 @@ final class InStock extends Constraint { public string $message = 'sylius.cart_item.not_available'; + public string $shortMessage = 'sylius.cart_item.insufficient_stock'; + public string $stockablePath = 'stockable'; public string $quantityPath = 'quantity'; @@ -28,8 +30,8 @@ final class InStock extends Constraint return 'sylius_in_stock'; } - public function getTargets(): string + public function getTargets(): array { - return self::CLASS_CONSTRAINT; + return [self::PROPERTY_CONSTRAINT, self::CLASS_CONSTRAINT]; } } diff --git a/src/Sylius/Bundle/InventoryBundle/Validator/Constraints/InStockValidator.php b/src/Sylius/Bundle/InventoryBundle/Validator/Constraints/InStockValidator.php index eac85d046e..c7eab0a9fb 100644 --- a/src/Sylius/Bundle/InventoryBundle/Validator/Constraints/InStockValidator.php +++ b/src/Sylius/Bundle/InventoryBundle/Validator/Constraints/InStockValidator.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\InventoryBundle\Validator\Constraints; +use Sylius\Component\Core\Model\OrderItemInterface; use Sylius\Component\Inventory\Checker\AvailabilityCheckerInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessor; @@ -29,26 +30,38 @@ final class InStockValidator extends ConstraintValidator $this->accessor = PropertyAccess::createPropertyAccessor(); } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var InStock $constraint */ Assert::isInstanceOf($constraint, InStock::class); - $stockable = $this->accessor->getValue($value, $constraint->stockablePath); + $target = is_int($value) ? Constraint::PROPERTY_CONSTRAINT : Constraint::CLASS_CONSTRAINT; + /** @var OrderItemInterface $object */ + $object = Constraint::PROPERTY_CONSTRAINT === $target ? $this->context->getObject() : $value; + + $stockable = $this->accessor->getValue($object, $constraint->stockablePath); if (null === $stockable) { return; } - $quantity = $this->accessor->getValue($value, $constraint->quantityPath); + $quantity = Constraint::CLASS_CONSTRAINT === $target ? $this->accessor->getValue($object, $constraint->quantityPath) : $value; if (null === $quantity) { return; } - if (!$this->availabilityChecker->isStockSufficient($stockable, $quantity)) { + if ($this->availabilityChecker->isStockSufficient($stockable, $quantity)) { + return; + } + + if (Constraint::CLASS_CONSTRAINT === $target) { $this->context->addViolation( $constraint->message, ['%itemName%' => $stockable->getInventoryName()], ); + + return; } + + $this->context->addViolation($constraint->shortMessage); } } diff --git a/src/Sylius/Bundle/InventoryBundle/composer.json b/src/Sylius/Bundle/InventoryBundle/composer.json index c9e0677fa2..145bccd242 100644 --- a/src/Sylius/Bundle/InventoryBundle/composer.json +++ b/src/Sylius/Bundle/InventoryBundle/composer.json @@ -26,12 +26,12 @@ } ], "require": { - "php": "^8.0", - "sylius/inventory": "^1.12", + "php": "^8.1", + "sylius/inventory": "^1.13", "sylius/resource-bundle": "^1.9", - "symfony/framework-bundle": "^5.4 || ^6.0", - "symfony/templating": "^5.4 || ^6.0", - "symfony/validator": "^5.4 || ^6.0" + "symfony/framework-bundle": "^5.4.21 || ^6.4", + "symfony/templating": "^5.4.21 || ^6.4", + "symfony/validator": "^5.4.21 || ^6.4" }, "conflict": { "doctrine/orm": ">= 2.16.0", @@ -40,10 +40,10 @@ "require-dev": { "doctrine/orm": "^2.13", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", + "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", "twig/twig": "^2.12 || ^3.3" }, "config": { @@ -53,7 +53,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { diff --git a/src/Sylius/Bundle/InventoryBundle/spec/Validator/Constraints/InStockSpec.php b/src/Sylius/Bundle/InventoryBundle/spec/Validator/Constraints/InStockSpec.php index 2fc81813df..73e8b15606 100644 --- a/src/Sylius/Bundle/InventoryBundle/spec/Validator/Constraints/InStockSpec.php +++ b/src/Sylius/Bundle/InventoryBundle/spec/Validator/Constraints/InStockSpec.php @@ -30,6 +30,6 @@ final class InStockSpec extends ObjectBehavior function it_has_a_target(): void { - $this->getTargets()->shouldReturn(Constraint::CLASS_CONSTRAINT); + $this->getTargets()->shouldReturn([Constraint::PROPERTY_CONSTRAINT, Constraint::CLASS_CONSTRAINT]); } } diff --git a/src/Sylius/Bundle/InventoryBundle/spec/Validator/Constraints/InStockValidatorSpec.php b/src/Sylius/Bundle/InventoryBundle/spec/Validator/Constraints/InStockValidatorSpec.php index c5e685fb01..de07cdd4db 100644 --- a/src/Sylius/Bundle/InventoryBundle/spec/Validator/Constraints/InStockValidatorSpec.php +++ b/src/Sylius/Bundle/InventoryBundle/spec/Validator/Constraints/InStockValidatorSpec.php @@ -20,12 +20,16 @@ use Sylius\Component\Inventory\Model\InventoryUnitInterface; use Sylius\Component\Inventory\Model\StockableInterface; use Symfony\Component\PropertyAccess\PropertyAccessor; use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Context\ExecutionContext; final class InStockValidatorSpec extends ObjectBehavior { - function let(AvailabilityCheckerInterface $availabilityChecker): void - { + function let( + AvailabilityCheckerInterface $availabilityChecker, + ExecutionContext $context, + ): void { $this->beConstructedWith($availabilityChecker); + $this->initialize($context); } function it_is_a_constraint_validator(): void @@ -39,9 +43,18 @@ final class InStockValidatorSpec extends ObjectBehavior ): void { $propertyAccessor->getValue($inventoryUnit, 'stockable')->willReturn(null); - $constraint = new InStock(); + $this->validate($inventoryUnit, new InStock()); + } - $this->validate($inventoryUnit, $constraint); + function it_does_not_add_violation_when_validating_number_and_there_is_no_stockable( + InventoryUnitInterface $inventoryUnit, + PropertyAccessor $propertyAccessor, + ExecutionContext $context, + ): void { + $context->getObject()->willReturn($inventoryUnit); + $propertyAccessor->getValue($inventoryUnit, 'stockable')->willReturn(null); + + $this->validate(1, new InStock()); } function it_does_not_add_violation_if_there_is_no_quantity( @@ -52,9 +65,20 @@ final class InStockValidatorSpec extends ObjectBehavior $propertyAccessor->getValue($inventoryUnit, 'stockable')->willReturn($stockable); $propertyAccessor->getValue($inventoryUnit, 'quantity')->willReturn(null); - $constraint = new InStock(); + $this->validate($inventoryUnit, new InStock()); + } - $this->validate($inventoryUnit, $constraint); + function it_does_not_add_violation_when_validating_number_and_there_is_no_quantity( + InventoryUnitInterface $inventoryUnit, + PropertyAccessor $propertyAccessor, + StockableInterface $stockable, + ExecutionContext $context, + ): void { + $context->getObject()->willReturn($inventoryUnit); + $propertyAccessor->getValue($inventoryUnit, 'stockable')->willReturn($stockable); + $propertyAccessor->getValue($inventoryUnit, 'quantity')->willReturn(null); + + $this->validate(1, new InStock()); } function it_does_not_add_violation_if_stock_is_sufficient( @@ -68,8 +92,22 @@ final class InStockValidatorSpec extends ObjectBehavior $availabilityChecker->isStockSufficient($stockable, 1)->willReturn(true); - $constraint = new InStock(); + $this->validate($inventoryUnit, new InStock()); + } - $this->validate($inventoryUnit, $constraint); + function it_does_not_add_violation_when_validating_number_and_stock_is_sufficient( + AvailabilityCheckerInterface $availabilityChecker, + InventoryUnitInterface $inventoryUnit, + PropertyAccessor $propertyAccessor, + StockableInterface $stockable, + ExecutionContext $context, + ): void { + $context->getObject()->willReturn($inventoryUnit); + $propertyAccessor->getValue($inventoryUnit, 'stockable')->willReturn($stockable); + $propertyAccessor->getValue($inventoryUnit, 'quantity')->willReturn(2); + + $availabilityChecker->isStockSufficient($stockable, 1)->willReturn(true); + + $this->validate(1, new InStock()); } } diff --git a/src/Sylius/Bundle/LocaleBundle/Attribute/AsLocaleContext.php b/src/Sylius/Bundle/LocaleBundle/Attribute/AsLocaleContext.php new file mode 100644 index 0000000000..d2d8756536 --- /dev/null +++ b/src/Sylius/Bundle/LocaleBundle/Attribute/AsLocaleContext.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/LocaleBundle/Checker/LocaleUsageChecker.php b/src/Sylius/Bundle/LocaleBundle/Checker/LocaleUsageChecker.php new file mode 100644 index 0000000000..cc54a7d709 --- /dev/null +++ b/src/Sylius/Bundle/LocaleBundle/Checker/LocaleUsageChecker.php @@ -0,0 +1,86 @@ + $localeRepository + */ + public function __construct( + private RepositoryInterface $localeRepository, + private RegistryInterface $resourceRegistry, + private EntityManagerInterface $entityManager, + ) { + } + + /** + * @throws LocaleNotFoundException + */ + public function isUsed(string $localeCode): bool + { + $locale = $this->localeRepository->findOneBy(['code' => $localeCode]); + + if (null === $locale) { + throw new LocaleNotFoundException(); + } + + $translationEntityInterfaces = $this->getTranslationEntityInterfaces(); + + foreach ($translationEntityInterfaces as $entityInterface) { + if ($this->isLocaleUsedByTranslation($entityInterface, $localeCode)) { + return true; + } + } + + return false; + } + + /** + * @return array + */ + private function getTranslationEntityInterfaces(): array + { + $translationEntityInterfaces = []; + + foreach ($this->resourceRegistry->getAll() as $resource) { + $resourceParameters = $resource->getParameters(); + + if (isset($resourceParameters['translation']['classes']['interface'])) { + $translationEntityInterfaces[] = $resourceParameters['translation']['classes']['interface']; + } + } + + return $translationEntityInterfaces; + } + + /** + * @param class-string $translationEntityInterface + */ + private function isLocaleUsedByTranslation(string $translationEntityInterface, string $localeCode): bool + { + /** @var EntityRepository $repository */ + $repository = $this->entityManager->getRepository($translationEntityInterface); + + return $repository->count(['locale' => $localeCode]) > 0; + } +} diff --git a/src/Sylius/Bundle/LocaleBundle/Checker/LocaleUsageCheckerInterface.php b/src/Sylius/Bundle/LocaleBundle/Checker/LocaleUsageCheckerInterface.php new file mode 100644 index 0000000000..bbd3470c26 --- /dev/null +++ b/src/Sylius/Bundle/LocaleBundle/Checker/LocaleUsageCheckerInterface.php @@ -0,0 +1,19 @@ +setParameter('sylius_locale.locale', $config['locale']); $container->findDefinition('sylius.repository.locale')->setLazy(true); + + $this->registerAutoconfiguration($container); + } + + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsLocaleContext::class, + static function (ChildDefinition $definition, AsLocaleContext $attribute): void { + $definition->addTag(AsLocaleContext::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); } } diff --git a/src/Sylius/Bundle/LocaleBundle/Doctrine/EventListener/LocaleModificationListener.php b/src/Sylius/Bundle/LocaleBundle/Doctrine/EventListener/LocaleModificationListener.php new file mode 100644 index 0000000000..911ecf3ed2 --- /dev/null +++ b/src/Sylius/Bundle/LocaleBundle/Doctrine/EventListener/LocaleModificationListener.php @@ -0,0 +1,29 @@ +cache->delete(CachedLocaleCollectionProvider::LOCALES_CACHE_KEY); + } +} diff --git a/src/Sylius/Bundle/LocaleBundle/Form/DataTransformer/LocaleToCodeTransformer.php b/src/Sylius/Bundle/LocaleBundle/Form/DataTransformer/LocaleToCodeTransformer.php new file mode 100644 index 0000000000..e3e4f2c932 --- /dev/null +++ b/src/Sylius/Bundle/LocaleBundle/Form/DataTransformer/LocaleToCodeTransformer.php @@ -0,0 +1,56 @@ +getCode(); + } + + public function reverseTransform(mixed $value): ?LocaleInterface + { + if (!is_string($value)) { + return null; + } + + return $this->getLocaleByCode($value); + } + + private function getLocaleByCode(string $localeCode): LocaleInterface + { + $locales = $this->localesProvider->getAll(); + + if (!isset($locales[$localeCode])) { + throw new TransformationFailedException(sprintf('Locale with code "%s" does not exist.', $localeCode)); + } + + return $locales[$localeCode]; + } +} diff --git a/src/Sylius/Bundle/LocaleBundle/Resources/config/services.xml b/src/Sylius/Bundle/LocaleBundle/Resources/config/services.xml index d5d4633d50..c990e51e2c 100644 --- a/src/Sylius/Bundle/LocaleBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/LocaleBundle/Resources/config/services.xml @@ -27,6 +27,10 @@ + + + + @@ -45,8 +49,17 @@ - + + + + + + + + + + %sylius_locale.locale% @@ -70,5 +83,18 @@ + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/LocaleBundle/Templating/Helper/LocaleHelper.php b/src/Sylius/Bundle/LocaleBundle/Templating/Helper/LocaleHelper.php index fc79190d65..2417be4ed8 100644 --- a/src/Sylius/Bundle/LocaleBundle/Templating/Helper/LocaleHelper.php +++ b/src/Sylius/Bundle/LocaleBundle/Templating/Helper/LocaleHelper.php @@ -25,7 +25,11 @@ final class LocaleHelper extends Helper implements LocaleHelperInterface private ?LocaleContextInterface $localeContext = null, ) { if (null === $localeContext) { - @trigger_error('Not passing LocaleContextInterface explicitly as the second argument is deprecated since 1.4 and will be prohibited in 2.0', \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/locale-bundle', + '1.4', + 'Not passing a $localeContext explicitly as the second argument is deprecated and will be prohibited in Sylius 2.0', + ); } } diff --git a/src/Sylius/Bundle/LocaleBundle/Tests/DependencyInjection/SyliusLocaleExtensionTest.php b/src/Sylius/Bundle/LocaleBundle/Tests/DependencyInjection/SyliusLocaleExtensionTest.php new file mode 100644 index 0000000000..a282c0e6fc --- /dev/null +++ b/src/Sylius/Bundle/LocaleBundle/Tests/DependencyInjection/SyliusLocaleExtensionTest.php @@ -0,0 +1,48 @@ +container->setDefinition( + 'acme.locale_context_with_attribute', + (new Definition()) + ->setClass(LocaleContextStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.locale_context_with_attribute', + AsLocaleContext::SERVICE_TAG, + ['priority' => 15], + ); + } + + protected function getContainerExtensions(): array + { + return [new SyliusLocaleExtension()]; + } +} diff --git a/src/Sylius/Bundle/LocaleBundle/Tests/Stub/LocaleContextStub.php b/src/Sylius/Bundle/LocaleBundle/Tests/Stub/LocaleContextStub.php new file mode 100644 index 0000000000..e05b720f9f --- /dev/null +++ b/src/Sylius/Bundle/LocaleBundle/Tests/Stub/LocaleContextStub.php @@ -0,0 +1,26 @@ += 2.16.0", @@ -38,12 +39,13 @@ }, "require-dev": { "doctrine/orm": "^2.13", + "matthiasnoback/symfony-dependency-injection-test": "^4.2", "friendsofphp/proxy-manager-lts": "^1.0.7", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", + "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", "twig/twig": "^2.12 || ^3.3" }, "config": { @@ -53,7 +55,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { @@ -63,7 +65,8 @@ }, "autoload-dev": { "psr-4": { - "Sylius\\Bundle\\LocaleBundle\\spec\\": "spec/" + "Sylius\\Bundle\\LocaleBundle\\spec\\": "spec/", + "Sylius\\Bundle\\LocaleBundle\\Tests\\": "Tests/" }, "classmap": [ "test/app/AppKernel.php" diff --git a/src/Sylius/Bundle/LocaleBundle/spec/Checker/LocaleUsageCheckerSpec.php b/src/Sylius/Bundle/LocaleBundle/spec/Checker/LocaleUsageCheckerSpec.php new file mode 100644 index 0000000000..b36c443dca --- /dev/null +++ b/src/Sylius/Bundle/LocaleBundle/spec/Checker/LocaleUsageCheckerSpec.php @@ -0,0 +1,96 @@ +beConstructedWith($localeRepository, $registry, $entityManager); + } + + function it_throws_exception_when_locale_with_provided_locale_code_doesnt_exist( + RepositoryInterface $localeRepository, + ): void { + $localeRepository->findOneBy(['code' => 'en_US'])->willReturn(null); + + $this->shouldThrow(LocaleNotFoundException::class)->during('isUsed', ['en_US']); + } + + function it_returns_true_when_at_least_one_usage_of_locale_found( + EntityRepository $localeRepository, + RegistryInterface $registry, + EntityManagerInterface $entityManager, + LocaleInterface $locale, + MetadataInterface $firstResourceMetadata, + MetadataInterface $secondResourceMetadata, + ): void { + $localeRepository->findOneBy(['code' => 'en_US'])->willReturn($locale); + $localeRepository->count(['locale' => 'en_US'])->willReturn(1); + + $registry->getAll()->willReturn([$firstResourceMetadata, $secondResourceMetadata]); + + $firstResourceMetadata->getParameters()->willReturn([ + 'translation' => [ + 'classes' => [ + 'interface' => 'Sylius\Component\Locale\Model\LocaleInterface', + ], + ], + ]); + $secondResourceMetadata->getParameters()->willReturn([]); + + $entityManager->getRepository('Sylius\Component\Locale\Model\LocaleInterface')->willReturn($localeRepository); + + $this->isUsed('en_US')->shouldReturn(true); + } + + function it_returns_false_when_no_usage_of_locale_found( + EntityRepository $localeRepository, + RegistryInterface $registry, + EntityManagerInterface $entityManager, + LocaleInterface $locale, + MetadataInterface $firstResourceMetadata, + MetadataInterface $secondResourceMetadata, + ): void { + $localeRepository->findOneBy(['code' => 'en_US'])->willReturn($locale); + $localeRepository->count(['locale' => 'en_US'])->willReturn(0); + + $registry->getAll()->willReturn([$firstResourceMetadata, $secondResourceMetadata]); + + $firstResourceMetadata->getParameters()->willReturn([ + 'translation' => [ + 'classes' => [ + 'interface' => 'Sylius\Component\Locale\Model\LocaleInterface', + ], + ], + ]); + $secondResourceMetadata->getParameters()->willReturn([]); + + $entityManager->getRepository('Sylius\Component\Locale\Model\LocaleInterface')->willReturn($localeRepository); + + $this->isUsed('en_US')->shouldReturn(false); + } +} diff --git a/src/Sylius/Bundle/LocaleBundle/spec/Doctrine/EventListener/LocaleModificationListenerSpec.php b/src/Sylius/Bundle/LocaleBundle/spec/Doctrine/EventListener/LocaleModificationListenerSpec.php new file mode 100644 index 0000000000..78e6b0c19f --- /dev/null +++ b/src/Sylius/Bundle/LocaleBundle/spec/Doctrine/EventListener/LocaleModificationListenerSpec.php @@ -0,0 +1,32 @@ +beConstructedWith($cache); + } + + function it_invalidates_cache(CacheInterface $cache): void + { + $cache->delete('sylius_locales')->shouldBeCalled(); + + $this->invalidateCachedLocales(); + } +} diff --git a/src/Sylius/Bundle/MoneyBundle/composer.json b/src/Sylius/Bundle/MoneyBundle/composer.json index baa3aa6fdc..9e8f73288d 100644 --- a/src/Sylius/Bundle/MoneyBundle/composer.json +++ b/src/Sylius/Bundle/MoneyBundle/composer.json @@ -25,11 +25,11 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "sylius/resource-bundle": "^1.9", - "symfony/framework-bundle": "^5.4 || ^6.0", - "symfony/intl": "^5.4 || ^6.0", - "symfony/templating": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4.21 || ^6.4", + "symfony/intl": "^5.4.21 || ^6.4", + "symfony/templating": "^5.4.21 || ^6.4", "webmozart/assert": "^1.9" }, "conflict": { @@ -39,12 +39,12 @@ "require-dev": { "doctrine/orm": "^2.13", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", + "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", - "sylius/currency-bundle": "^1.12", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0", + "sylius/currency-bundle": "^1.13", + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4", "twig/twig": "^2.12 || ^3.3" }, "config": { @@ -54,7 +54,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { diff --git a/src/Sylius/Bundle/OrderBundle/Attribute/AsCartContext.php b/src/Sylius/Bundle/OrderBundle/Attribute/AsCartContext.php new file mode 100644 index 0000000000..fc8ca20d07 --- /dev/null +++ b/src/Sylius/Bundle/OrderBundle/Attribute/AsCartContext.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/OrderBundle/Attribute/AsOrderProcessor.php b/src/Sylius/Bundle/OrderBundle/Attribute/AsOrderProcessor.php new file mode 100644 index 0000000000..04caf1da86 --- /dev/null +++ b/src/Sylius/Bundle/OrderBundle/Attribute/AsOrderProcessor.php @@ -0,0 +1,30 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/OrderBundle/Command/RemoveExpiredCartsCommand.php b/src/Sylius/Bundle/OrderBundle/Command/RemoveExpiredCartsCommand.php index 872c1275ab..d7755abfff 100644 --- a/src/Sylius/Bundle/OrderBundle/Command/RemoveExpiredCartsCommand.php +++ b/src/Sylius/Bundle/OrderBundle/Command/RemoveExpiredCartsCommand.php @@ -13,35 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\OrderBundle\Command; -use SyliusLabs\Polyfill\Symfony\FrameworkBundle\Command\ContainerAwareCommand; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; +trigger_deprecation( + 'sylius/order-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + RemoveExpiredCartsCommand::class, + \Sylius\Bundle\OrderBundle\Console\Command\RemoveExpiredCartsCommand::class, +); -/** - * @final - */ -class RemoveExpiredCartsCommand extends ContainerAwareCommand -{ - protected static $defaultName = 'sylius:remove-expired-carts'; +class_exists(\Sylius\Bundle\OrderBundle\Console\Command\RemoveExpiredCartsCommand::class); - protected function configure(): void +if (false) { + final class RemoveExpiredCartsCommand { - $this - ->setDescription('Removes carts that have been idle for a period set in `sylius_order.expiration.cart` configuration key.') - ; - } - - protected function execute(InputInterface $input, OutputInterface $output) - { - $expirationTime = $this->getContainer()->getParameter('sylius_order.cart_expiration_period'); - $output->writeln(sprintf( - 'Command will remove carts that have been idle for %s.', - (string) $expirationTime, - )); - - $expiredCartsRemover = $this->getContainer()->get('sylius.expired_carts_remover'); - $expiredCartsRemover->remove(); - - return 0; } } diff --git a/src/Sylius/Bundle/OrderBundle/Console/Command/RemoveExpiredCartsCommand.php b/src/Sylius/Bundle/OrderBundle/Console/Command/RemoveExpiredCartsCommand.php new file mode 100644 index 0000000000..4449cc6c40 --- /dev/null +++ b/src/Sylius/Bundle/OrderBundle/Console/Command/RemoveExpiredCartsCommand.php @@ -0,0 +1,78 @@ +setDescription('Removes carts that have been idle for a period set in `sylius_order.expiration.cart` configuration key.') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if ($this->expirationTime === null) { + trigger_deprecation( + 'sylius/order-bundle', + '1.12', + 'Not injecting the expiration time into %s is deprecated and will be mandatory from Sylius 2.0', + self::class, + ); + $expirationTime = $this->getContainer()->getParameter('sylius_order.cart_expiration_period'); + } else { + $expirationTime = $this->expirationTime; + } + + $output->writeln(sprintf( + 'Command will remove carts that have been idle for %s.', + (string) $expirationTime, + )); + + if ($this->expiredCartsRemover === null) { + trigger_deprecation( + 'sylius/order-bundle', + '1.12', + 'Not injecting the %s into the %s is deprecated and will be mandatory from Sylius 2.0', + ExpiredCartsRemoverInterface::class, + self::class, + ); + $this->getContainer()->get('sylius.expired_carts_remover')->remove(); + } else { + $this->expiredCartsRemover->remove(); + } + + return 0; + } +} + +class_alias(RemoveExpiredCartsCommand::class, \Sylius\Bundle\OrderBundle\Command\RemoveExpiredCartsCommand::class); diff --git a/src/Sylius/Bundle/OrderBundle/Controller/OrderController.php b/src/Sylius/Bundle/OrderBundle/Controller/OrderController.php index b2e616c312..72fa92a2e6 100644 --- a/src/Sylius/Bundle/OrderBundle/Controller/OrderController.php +++ b/src/Sylius/Bundle/OrderBundle/Controller/OrderController.php @@ -25,6 +25,8 @@ use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\HttpKernel\Exception\HttpException; class OrderController extends ResourceController @@ -38,6 +40,13 @@ class OrderController extends ResourceController $cart = $this->getOrderRepository()->findCartById($cart->getId()); } + $this->getEventDispatcher()->dispatch(new GenericEvent($cart), SyliusCartEvents::CART_SUMMARY); + $event = $this->eventDispatcher->dispatch(ResourceActions::SHOW, $configuration, $cart); + $eventResponse = $event->getResponse(); + if (null !== $eventResponse) { + return $eventResponse; + } + if (!$configuration->isHtmlRequest()) { return $this->viewHandler->handle($configuration, View::create($cart)); } @@ -114,6 +123,7 @@ class OrderController extends ResourceController if ($form->isSubmitted() && !$form->isValid()) { $this->resetChangesOnCart($resource); + $this->addFlash('error', 'sylius.cart.not_recalculated'); } if (!$configuration->isHtmlRequest()) { @@ -131,6 +141,15 @@ class OrderController extends ResourceController ); } + protected function addFlash(string $type, $message): void + { + /** @var SessionInterface $session */ + $session = $this->get('request_stack')->getSession(); + /** @var FlashBagInterface $flashBag */ + $flashBag = $session->getBag('flashes'); + $flashBag->add($type, $message); + } + private function resetChangesOnCart(OrderInterface $cart): void { if (!$this->manager->contains($cart)) { @@ -179,6 +198,12 @@ class OrderController extends ResourceController protected function redirectToCartSummary(RequestConfiguration $configuration): Response { + trigger_deprecation( + 'sylius/order-bundle', + '1.13', + 'The %s::redirectToCartSummary() method is deprecated and will be removed in Sylius 2.0.', + self::class, + ); if (null === $configuration->getParameters()->get('redirect')) { return $this->redirectHandler->redirectToRoute($configuration, $this->getCartSummaryRoute()); } diff --git a/src/Sylius/Bundle/OrderBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/OrderBundle/DependencyInjection/Configuration.php index 658b7a3a4a..3cfd0ad775 100644 --- a/src/Sylius/Bundle/OrderBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/OrderBundle/DependencyInjection/Configuration.php @@ -46,6 +46,7 @@ final class Configuration implements ConfigurationInterface ->addDefaultsIfNotSet() ->children() ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() + ->scalarNode('autoconfigure_with_attributes')->defaultFalse()->end() ->end() ; diff --git a/src/Sylius/Bundle/OrderBundle/DependencyInjection/SyliusOrderExtension.php b/src/Sylius/Bundle/OrderBundle/DependencyInjection/SyliusOrderExtension.php index 29acd46240..a404f73fa4 100644 --- a/src/Sylius/Bundle/OrderBundle/DependencyInjection/SyliusOrderExtension.php +++ b/src/Sylius/Bundle/OrderBundle/DependencyInjection/SyliusOrderExtension.php @@ -13,12 +13,15 @@ declare(strict_types=1); namespace Sylius\Bundle\OrderBundle\DependencyInjection; +use Sylius\Bundle\OrderBundle\Attribute\AsCartContext; +use Sylius\Bundle\OrderBundle\Attribute\AsOrderProcessor; use Sylius\Bundle\OrderBundle\DependencyInjection\Compiler\RegisterCartContextsPass; use Sylius\Bundle\OrderBundle\DependencyInjection\Compiler\RegisterProcessorsPass; use Sylius\Bundle\ResourceBundle\DependencyInjection\Extension\AbstractResourceExtension; use Sylius\Component\Order\Context\CartContextInterface; use Sylius\Component\Order\Processor\OrderProcessorInterface; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -38,13 +41,33 @@ final class SyliusOrderExtension extends AbstractResourceExtension $container->setParameter('sylius_order.cart_expiration_period', $config['expiration']['cart']); $container->setParameter('sylius_order.order_expiration_period', $config['expiration']['order']); - $container - ->registerForAutoconfiguration(CartContextInterface::class) - ->addTag(RegisterCartContextsPass::CART_CONTEXT_SERVICE_TAG) - ; - $container - ->registerForAutoconfiguration(OrderProcessorInterface::class) - ->addTag(RegisterProcessorsPass::PROCESSOR_SERVICE_TAG) - ; + $this->registerAutoconfiguration($container, $config['autoconfigure_with_attributes']); + } + + private function registerAutoconfiguration(ContainerBuilder $container, bool $autoconfigureWithAttributes): void + { + if (true === $autoconfigureWithAttributes) { + $container->registerAttributeForAutoconfiguration( + AsCartContext::class, + static function (ChildDefinition $definition, AsCartContext $attribute): void { + $definition->addTag(AsCartContext::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + $container->registerAttributeForAutoconfiguration( + AsOrderProcessor::class, + static function (ChildDefinition $definition, AsOrderProcessor $attribute): void { + $definition->addTag(AsOrderProcessor::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + } else { + $container + ->registerForAutoconfiguration(CartContextInterface::class) + ->addTag(RegisterCartContextsPass::CART_CONTEXT_SERVICE_TAG) + ; + $container + ->registerForAutoconfiguration(OrderProcessorInterface::class) + ->addTag(RegisterProcessorsPass::PROCESSOR_SERVICE_TAG) + ; + } } } diff --git a/src/Sylius/Bundle/OrderBundle/Doctrine/ORM/OrderItemRepository.php b/src/Sylius/Bundle/OrderBundle/Doctrine/ORM/OrderItemRepository.php index 626d13210a..12124349ca 100644 --- a/src/Sylius/Bundle/OrderBundle/Doctrine/ORM/OrderItemRepository.php +++ b/src/Sylius/Bundle/OrderBundle/Doctrine/ORM/OrderItemRepository.php @@ -18,6 +18,11 @@ use Sylius\Component\Order\Model\OrderInterface; use Sylius\Component\Order\Model\OrderItemInterface; use Sylius\Component\Order\Repository\OrderItemRepositoryInterface; +/** + * @template T of OrderItemInterface + * + * @implements OrderItemRepositoryInterface + */ class OrderItemRepository extends EntityRepository implements OrderItemRepositoryInterface { public function findOneByIdAndCartId($id, $cartId): ?OrderItemInterface diff --git a/src/Sylius/Bundle/OrderBundle/Doctrine/ORM/OrderRepository.php b/src/Sylius/Bundle/OrderBundle/Doctrine/ORM/OrderRepository.php index 0b2aaf6c20..6d7932d76a 100644 --- a/src/Sylius/Bundle/OrderBundle/Doctrine/ORM/OrderRepository.php +++ b/src/Sylius/Bundle/OrderBundle/Doctrine/ORM/OrderRepository.php @@ -19,6 +19,11 @@ use Sylius\Component\Order\Model\OrderInterface; use Sylius\Component\Order\Repository\OrderRepositoryInterface; use Webmozart\Assert\Assert; +/** + * @template T of OrderInterface + * + * @implements OrderRepositoryInterface + */ class OrderRepository extends EntityRepository implements OrderRepositoryInterface { public function countPlacedOrders(): int @@ -91,7 +96,7 @@ class OrderRepository extends EntityRepository implements OrderRepositoryInterfa ; } - /** @deprecated since 1.9 and will be removed in Sylius 2.0, use src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderRepositoryInterface instead */ + /** @deprecated since Sylius 1.9 and will be removed in Sylius 2.0, use src/Sylius/Bundle/CoreBundle/Doctrine/ORM/OrderRepositoryInterface instead */ public function findCartByTokenValue(string $tokenValue): ?OrderInterface { return $this->createQueryBuilder('o') diff --git a/src/Sylius/Bundle/OrderBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/OrderBundle/Resources/config/app/config.yml index 3324c6cea7..c2e19770f0 100644 --- a/src/Sylius/Bundle/OrderBundle/Resources/config/app/config.yml +++ b/src/Sylius/Bundle/OrderBundle/Resources/config/app/config.yml @@ -1,9 +1,18 @@ # This file is part of the Sylius package. # (c) Sylius Sp. z o.o. +parameters: + sylius_order.resend_order_confirmation_email.order_states: + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + jms_serializer: metadata: directories: sylius-order: namespace_prefix: "Sylius\\Component\\Order" path: "@SyliusOrderBundle/Resources/config/serializer" + +twig: + globals: + sylius_order_states_that_allows_to_resend_order_confirmation_email: "%sylius_order.resend_order_confirmation_email.order_states%" + diff --git a/src/Sylius/Bundle/OrderBundle/Resources/config/services.xml b/src/Sylius/Bundle/OrderBundle/Resources/config/services.xml index fdef101d48..03492f9bed 100644 --- a/src/Sylius/Bundle/OrderBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/OrderBundle/Resources/config/services.xml @@ -21,10 +21,15 @@ - + + + %sylius_order.cart_expiration_period% + + + diff --git a/src/Sylius/Bundle/OrderBundle/Resources/config/workflow/state_machine.yaml b/src/Sylius/Bundle/OrderBundle/Resources/config/workflow/state_machine.yaml new file mode 100644 index 0000000000..5a88ef99d3 --- /dev/null +++ b/src/Sylius/Bundle/OrderBundle/Resources/config/workflow/state_machine.yaml @@ -0,0 +1,25 @@ +framework: + workflows: + !php/const Sylius\Component\Order\OrderTransitions::GRAPH: + type: state_machine + marking_store: + type: method + property: state + supports: + - Sylius\Component\Order\Model\OrderInterface + initial_marking: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CART + places: + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CART + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CANCELLED + - !php/const Sylius\Component\Order\Model\OrderInterface::STATE_FULFILLED + transitions: + !php/const Sylius\Component\Order\OrderTransitions::TRANSITION_CREATE: + from: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CART + to: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + !php/const Sylius\Component\Order\OrderTransitions::TRANSITION_CANCEL: + from: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + to: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_CANCELLED + !php/const Sylius\Component\Order\OrderTransitions::TRANSITION_FULFILL: + from: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_NEW + to: !php/const Sylius\Component\Order\Model\OrderInterface::STATE_FULFILLED diff --git a/src/Sylius/Bundle/OrderBundle/Resources/translations/flashes.en.yml b/src/Sylius/Bundle/OrderBundle/Resources/translations/flashes.en.yml index dd1c69211e..21bf1489b4 100644 --- a/src/Sylius/Bundle/OrderBundle/Resources/translations/flashes.en.yml +++ b/src/Sylius/Bundle/OrderBundle/Resources/translations/flashes.en.yml @@ -5,5 +5,6 @@ sylius: cart: add_item: 'Item has been added to cart' cannot_modify: 'An order has already been submitted for the cart you were trying to modify.' + not_recalculated: 'Due to invalid values in your cart, it has not been recalculated.' remove_item: 'Item has been removed from cart' save: 'The cart has been successfully updated' diff --git a/src/Sylius/Bundle/OrderBundle/Resources/translations/flashes.fr.yml b/src/Sylius/Bundle/OrderBundle/Resources/translations/flashes.fr.yml index 15820082cf..7efe1b6b22 100644 --- a/src/Sylius/Bundle/OrderBundle/Resources/translations/flashes.fr.yml +++ b/src/Sylius/Bundle/OrderBundle/Resources/translations/flashes.fr.yml @@ -5,5 +5,6 @@ sylius: cart: add_item: 'L''article a bien été ajouté au panier' cannot_modify: 'Une commande a déjà été soumise pour le panier que vous avez essayé de modifier.' + not_recalculated: 'En raison de valeurs invalides dans votre panier, il n''a pas été recalculé.' remove_item: 'L''article a bien été supprimé du panier' save: 'Le panier a été mis à jour avec succès' diff --git a/src/Sylius/Bundle/OrderBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/OrderBundle/Resources/translations/validators.en.yml index b984f947d6..c7ee1e52bb 100644 --- a/src/Sylius/Bundle/OrderBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/OrderBundle/Resources/translations/validators.en.yml @@ -1,4 +1,10 @@ +# This file is part of the Sylius package. +# (c) Sylius Sp. z o.o. + sylius: order_item: + product_variant: + not_blank: The product variant has not been provided. quantity: min: Quantity of an order item cannot be lower than 1. + not_blank: The quantity has not been provided. diff --git a/src/Sylius/Bundle/OrderBundle/Resources/translations/validators.fr.yml b/src/Sylius/Bundle/OrderBundle/Resources/translations/validators.fr.yml index 76b68ac858..fd7c566661 100644 --- a/src/Sylius/Bundle/OrderBundle/Resources/translations/validators.fr.yml +++ b/src/Sylius/Bundle/OrderBundle/Resources/translations/validators.fr.yml @@ -3,5 +3,8 @@ sylius: order_item: + product_variant: + not_blank: La variante du produit n'a pas été fournie. quantity: min: La quantité ne peut être inférieure à 1. + not_blank: La quantité n'a pas été fournie. diff --git a/src/Sylius/Bundle/OrderBundle/Tests/DependencyInjection/Compiler/RegisterOrderProcessorPassTest.php b/src/Sylius/Bundle/OrderBundle/Tests/DependencyInjection/Compiler/RegisterOrderProcessorPassTest.php index a01c662600..ba55641dad 100644 --- a/src/Sylius/Bundle/OrderBundle/Tests/DependencyInjection/Compiler/RegisterOrderProcessorPassTest.php +++ b/src/Sylius/Bundle/OrderBundle/Tests/DependencyInjection/Compiler/RegisterOrderProcessorPassTest.php @@ -17,8 +17,8 @@ use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractCompilerPassTestCase use Matthias\SymfonyDependencyInjectionTest\PhpUnit\DefinitionHasMethodCallConstraint; use PHPUnit\Framework\Constraint\LogicalNot; use Sylius\Bundle\OrderBundle\DependencyInjection\Compiler\RegisterProcessorsPass; -use Sylius\Component\Core\OrderProcessing\OrderAdjustmentsClearer; use Sylius\Component\Order\Processor\CompositeOrderProcessor; +use Sylius\Component\Order\Processor\OrderProcessorInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; @@ -33,7 +33,7 @@ final class RegisterOrderProcessorPassTest extends AbstractCompilerPassTestCase $compositeOrderProcessorDefinition = new Definition(CompositeOrderProcessor::class); $this->setDefinition('sylius.order_processing.order_processor.composite', $compositeOrderProcessorDefinition); - $orderAdjustmentClearerDefinition = new Definition(OrderAdjustmentsClearer::class); + $orderAdjustmentClearerDefinition = new Definition(OrderProcessorInterface::class); $orderAdjustmentClearerDefinition->addTag('sylius.order_processor'); $this->setDefinition('sylius.order_processing.order_adjustments_clearer', $orderAdjustmentClearerDefinition); @@ -58,7 +58,7 @@ final class RegisterOrderProcessorPassTest extends AbstractCompilerPassTestCase $compositeOrderProcessorDefinition = new Definition(CompositeOrderProcessor::class); $this->setDefinition('sylius.order_processing.order_processor.composite', $compositeOrderProcessorDefinition); - $orderAdjustmentClearerDefinition = new Definition(OrderAdjustmentsClearer::class); + $orderAdjustmentClearerDefinition = new Definition(OrderProcessorInterface::class); $orderAdjustmentClearerDefinition->addTag('sylius.order_processor', ['priority' => 10]); $this->setDefinition('sylius.order_processing.order_adjustments_clearer', $orderAdjustmentClearerDefinition); diff --git a/src/Sylius/Bundle/OrderBundle/Tests/DependencyInjection/SyliusOrderExtensionTest.php b/src/Sylius/Bundle/OrderBundle/Tests/DependencyInjection/SyliusOrderExtensionTest.php index 8cc6427c02..e5dfbab960 100644 --- a/src/Sylius/Bundle/OrderBundle/Tests/DependencyInjection/SyliusOrderExtensionTest.php +++ b/src/Sylius/Bundle/OrderBundle/Tests/DependencyInjection/SyliusOrderExtensionTest.php @@ -17,6 +17,8 @@ use Matthias\SymfonyDependencyInjectionTest\PhpUnit\AbstractExtensionTestCase; use Sylius\Bundle\OrderBundle\DependencyInjection\Compiler\RegisterCartContextsPass; use Sylius\Bundle\OrderBundle\DependencyInjection\Compiler\RegisterProcessorsPass; use Sylius\Bundle\OrderBundle\DependencyInjection\SyliusOrderExtension; +use Sylius\Bundle\OrderBundle\Tests\Stub\CartContextWithAttributeStub; +use Sylius\Bundle\OrderBundle\Tests\Stub\OrderProcessorWithAttributeStub; use Sylius\Component\Order\Context\CartContextInterface; use Sylius\Component\Order\Processor\OrderProcessorInterface; use Symfony\Component\DependencyInjection\Definition; @@ -28,10 +30,11 @@ final class SyliusOrderExtensionTest extends AbstractExtensionTestCase */ public function it_autoconfigures_cart_contexts(): void { + $orderProcessor = $this->createMock(CartContextInterface::class); $this->container->setDefinition( 'acme.cart_context_autoconfigured', (new Definition()) - ->setClass($this->getMockClass(CartContextInterface::class)) + ->setClass(get_class($orderProcessor)) ->setAutoconfigured(true), ); @@ -47,12 +50,13 @@ final class SyliusOrderExtensionTest extends AbstractExtensionTestCase /** * @test */ - public function it_does_not_autoconfigure_order_processors(): void + public function it_autoconfigures_order_processors(): void { + $orderProcessor = $this->createMock(OrderProcessorInterface::class); $this->container->setDefinition( 'acme.processor_autoconfigured', (new Definition()) - ->setClass($this->getMockClass(OrderProcessorInterface::class)) + ->setClass(get_class($orderProcessor)) ->setAutoconfigured(true), ); @@ -65,6 +69,54 @@ final class SyliusOrderExtensionTest extends AbstractExtensionTestCase ); } + /** @test */ + public function it_autoconfigures_cart_context_with_attribute(): void + { + $this->container->loadFromExtension('sylius_order', [ + 'autoconfigure_with_attributes' => true, + ]); + + $this->container->setDefinition( + 'acme.cart_context_autoconfigured', + (new Definition()) + ->setClass(CartContextWithAttributeStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.cart_context_autoconfigured', + RegisterCartContextsPass::CART_CONTEXT_SERVICE_TAG, + ['priority' => 20], + ); + } + + /** @test */ + public function it_autoconfigures_order_processors_with_attribute(): void + { + $this->container->loadFromExtension('sylius_order', [ + 'autoconfigure_with_attributes' => true, + ]); + + $this->container->setDefinition( + 'acme.processor_autoconfigured', + (new Definition()) + ->setClass(OrderProcessorWithAttributeStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.processor_autoconfigured', + RegisterProcessorsPass::PROCESSOR_SERVICE_TAG, + ['priority' => 10], + ); + } + protected function getContainerExtensions(): array { return [new SyliusOrderExtension()]; diff --git a/src/Sylius/Bundle/OrderBundle/Tests/Stub/CartContextWithAttributeStub.php b/src/Sylius/Bundle/OrderBundle/Tests/Stub/CartContextWithAttributeStub.php new file mode 100644 index 0000000000..fdf34db039 --- /dev/null +++ b/src/Sylius/Bundle/OrderBundle/Tests/Stub/CartContextWithAttributeStub.php @@ -0,0 +1,28 @@ += 2.16.0", @@ -46,10 +46,10 @@ "doctrine/orm": "^2.13", "matthiasnoback/symfony-dependency-injection-test": "^4.2", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0", - "symfony/validator": "^5.4 || ^6.0" + "phpunit/phpunit": "^9.5", + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4", + "symfony/validator": "^5.4.21 || ^6.4" }, "config": { "allow-plugins": { @@ -58,7 +58,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { diff --git a/src/Sylius/Bundle/PaymentBundle/Attribute/AsPaymentMethodsResolver.php b/src/Sylius/Bundle/PaymentBundle/Attribute/AsPaymentMethodsResolver.php new file mode 100644 index 0000000000..f9ed90a6b4 --- /dev/null +++ b/src/Sylius/Bundle/PaymentBundle/Attribute/AsPaymentMethodsResolver.php @@ -0,0 +1,42 @@ +type; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getPriority(): int + { + return $this->priority; + } +} diff --git a/src/Sylius/Bundle/PaymentBundle/DependencyInjection/SyliusPaymentExtension.php b/src/Sylius/Bundle/PaymentBundle/DependencyInjection/SyliusPaymentExtension.php index 218ebe10df..c61062393d 100644 --- a/src/Sylius/Bundle/PaymentBundle/DependencyInjection/SyliusPaymentExtension.php +++ b/src/Sylius/Bundle/PaymentBundle/DependencyInjection/SyliusPaymentExtension.php @@ -13,8 +13,10 @@ declare(strict_types=1); namespace Sylius\Bundle\PaymentBundle\DependencyInjection; +use Sylius\Bundle\PaymentBundle\Attribute\AsPaymentMethodsResolver; use Sylius\Bundle\ResourceBundle\DependencyInjection\Extension\AbstractResourceExtension; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -30,5 +32,21 @@ final class SyliusPaymentExtension extends AbstractResourceExtension $loader->load('services.xml'); $container->setParameter('sylius.payment_gateways', $config['gateways']); + + $this->registerAutoconfiguration($container); + } + + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsPaymentMethodsResolver::class, + static function (ChildDefinition $definition, AsPaymentMethodsResolver $attribute): void { + $definition->addTag(AsPaymentMethodsResolver::SERVICE_TAG, [ + 'type' => $attribute->getType(), + 'label' => $attribute->getLabel(), + 'priority' => $attribute->getPriority(), + ]); + }, + ); } } diff --git a/src/Sylius/Bundle/PaymentBundle/Doctrine/ORM/PaymentMethodRepository.php b/src/Sylius/Bundle/PaymentBundle/Doctrine/ORM/PaymentMethodRepository.php index f81f4b5803..055dc32260 100644 --- a/src/Sylius/Bundle/PaymentBundle/Doctrine/ORM/PaymentMethodRepository.php +++ b/src/Sylius/Bundle/PaymentBundle/Doctrine/ORM/PaymentMethodRepository.php @@ -14,8 +14,14 @@ declare(strict_types=1); namespace Sylius\Bundle\PaymentBundle\Doctrine\ORM; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Payment\Model\PaymentMethodInterface; use Sylius\Component\Payment\Repository\PaymentMethodRepositoryInterface; +/** + * @template T of PaymentMethodInterface + * + * @implements PaymentMethodRepositoryInterface + */ class PaymentMethodRepository extends EntityRepository implements PaymentMethodRepositoryInterface { public function findByName(string $name, string $locale): array diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/PaymentBundle/Resources/config/app/config.yml index e5bfe497fe..3615048869 100644 --- a/src/Sylius/Bundle/PaymentBundle/Resources/config/app/config.yml +++ b/src/Sylius/Bundle/PaymentBundle/Resources/config/app/config.yml @@ -7,3 +7,13 @@ jms_serializer: sylius-payment: namespace_prefix: "Sylius\\Component\\Payment" path: "@SyliusPaymentBundle/Resources/config/serializer" + +sylius_payum: + gateway_config: + validation_groups: + paypal_express_checkout: + - 'sylius' + - 'sylius_paypal_express_checkout' + stripe_checkout: + - 'sylius' + - 'sylius_stripe_checkout' diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/config/app/state_machine.yml b/src/Sylius/Bundle/PaymentBundle/Resources/config/app/state_machine.yml index 3adf006970..bc2248589c 100644 --- a/src/Sylius/Bundle/PaymentBundle/Resources/config/app/state_machine.yml +++ b/src/Sylius/Bundle/PaymentBundle/Resources/config/app/state_machine.yml @@ -11,10 +11,11 @@ winzou_state_machine: cart: ~ new: ~ processing: ~ + authorized: ~ completed: ~ failed: ~ cancelled: ~ - void: ~ + unknown: ~ refunded: ~ transitions: create: @@ -23,18 +24,21 @@ winzou_state_machine: process: from: [new] to: processing - complete: + authorize: from: [new, processing] + to: authorized + complete: + from: [new, processing, authorized] to: completed fail: - from: [new, processing] + from: [new, processing, authorized] to: failed cancel: - from: [new, processing] + from: [new, processing, authorized] to: cancelled refund: from: [completed] to: refunded void: - from: [new, processing] - to: void + from: [new, processing, authorized] + to: unknown diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/config/validation/PaymentMethodTranslation.xml b/src/Sylius/Bundle/PaymentBundle/Resources/config/validation/PaymentMethodTranslation.xml index 8edb7ae8e8..f9b2b67a92 100644 --- a/src/Sylius/Bundle/PaymentBundle/Resources/config/validation/PaymentMethodTranslation.xml +++ b/src/Sylius/Bundle/PaymentBundle/Resources/config/validation/PaymentMethodTranslation.xml @@ -13,6 +13,14 @@ + + + + + @@ -26,5 +34,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/config/workflow/state_machine.yaml b/src/Sylius/Bundle/PaymentBundle/Resources/config/workflow/state_machine.yaml new file mode 100644 index 0000000000..31f7396b86 --- /dev/null +++ b/src/Sylius/Bundle/PaymentBundle/Resources/config/workflow/state_machine.yaml @@ -0,0 +1,57 @@ +framework: + workflows: + !php/const Sylius\Component\Payment\PaymentTransitions::GRAPH: + type: state_machine + marking_store: + type: method + property: state + supports: + - Sylius\Component\Payment\Model\PaymentInterface + initial_marking: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_CART + places: + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_CART + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_NEW + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_PROCESSING + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_AUTHORIZED + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_COMPLETED + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_FAILED + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_CANCELLED + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_UNKNOWN + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_REFUNDED + transitions: + !php/const Sylius\Component\Payment\PaymentTransitions::TRANSITION_CREATE: + from: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_CART + to: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_NEW + !php/const Sylius\Component\Payment\PaymentTransitions::TRANSITION_PROCESS: + from: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_NEW + to: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_PROCESSING + !php/const Sylius\Component\Payment\PaymentTransitions::TRANSITION_AUTHORIZE: + from: + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_NEW + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_PROCESSING + to: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_AUTHORIZED + !php/const Sylius\Component\Payment\PaymentTransitions::TRANSITION_COMPLETE: + from: + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_NEW + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_PROCESSING + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_AUTHORIZED + to: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_COMPLETED + !php/const Sylius\Component\Payment\PaymentTransitions::TRANSITION_FAIL: + from: + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_NEW + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_PROCESSING + to: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_FAILED + !php/const Sylius\Component\Payment\PaymentTransitions::TRANSITION_CANCEL: + from: + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_NEW + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_PROCESSING + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_AUTHORIZED + to: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_CANCELLED + !php/const Sylius\Component\Payment\PaymentTransitions::TRANSITION_REFUND: + from: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_COMPLETED + to: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_REFUNDED + !php/const Sylius\Component\Payment\PaymentTransitions::TRANSITION_VOID: + from: + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_NEW + - !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_PROCESSING + to: !php/const Sylius\Component\Payment\Model\PaymentInterface::STATE_UNKNOWN diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/PaymentBundle/Resources/translations/validators.en.yml index f2219924e4..36d5926a86 100644 --- a/src/Sylius/Bundle/PaymentBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/PaymentBundle/Resources/translations/validators.en.yml @@ -40,3 +40,8 @@ sylius: not_blank: 'Please enter payment method code.' regex: 'Payment method code can only be comprised of letters, numbers, dashes and underscores.' unique: 'The payment method with given code already exists.' + translation: + locale: + not_blank: Please enter the locale. + invalid: This value is not a valid locale. + unique: A translation for the {{ value }} locale code already exists. diff --git a/src/Sylius/Bundle/PaymentBundle/Resources/translations/validators.fr.yml b/src/Sylius/Bundle/PaymentBundle/Resources/translations/validators.fr.yml index e47d72f25b..e7d6ac3878 100644 --- a/src/Sylius/Bundle/PaymentBundle/Resources/translations/validators.fr.yml +++ b/src/Sylius/Bundle/PaymentBundle/Resources/translations/validators.fr.yml @@ -43,3 +43,8 @@ sylius: not_blank: 'Veuillez entrez le code du moyen de paiement.' regex: 'Le code du moyen de paiement peut uniquement être constitué de lettres, chiffres, tirets et tirets bas.' unique: 'Le moyen de paiement avec le code attribué existe déjà.' + translation: + locale: + not_blank: Veuillez entrer la locale. + invalid: Cette valeur n'est pas une locale valide. + unique: Une traduction pour la locale {{ value }} existe déjà. diff --git a/src/Sylius/Bundle/PaymentBundle/Tests/DependencyInjection/SyliusPaymentExtensionTest.php b/src/Sylius/Bundle/PaymentBundle/Tests/DependencyInjection/SyliusPaymentExtensionTest.php new file mode 100644 index 0000000000..1fc34cd505 --- /dev/null +++ b/src/Sylius/Bundle/PaymentBundle/Tests/DependencyInjection/SyliusPaymentExtensionTest.php @@ -0,0 +1,48 @@ +container->setDefinition( + 'acme.payment_methods_resolver_with_attribute', + (new Definition()) + ->setClass(PaymentMethodsResolverStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.payment_methods_resolver_with_attribute', + AsPaymentMethodsResolver::SERVICE_TAG, + ['type' => 'test', 'label' => 'Test', 'priority' => 15], + ); + } + + protected function getContainerExtensions(): array + { + return [new SyliusPaymentExtension()]; + } +} diff --git a/src/Sylius/Bundle/PaymentBundle/Tests/Stub/PaymentMethodsResolverStub.php b/src/Sylius/Bundle/PaymentBundle/Tests/Stub/PaymentMethodsResolverStub.php new file mode 100644 index 0000000000..bbf13a57ed --- /dev/null +++ b/src/Sylius/Bundle/PaymentBundle/Tests/Stub/PaymentMethodsResolverStub.php @@ -0,0 +1,32 @@ +type; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getPriority(): int + { + return $this->priority; + } +} diff --git a/src/Sylius/Bundle/PayumBundle/Controller/PayumController.php b/src/Sylius/Bundle/PayumBundle/Controller/PayumController.php index c1f2a44b66..415012e803 100644 --- a/src/Sylius/Bundle/PayumBundle/Controller/PayumController.php +++ b/src/Sylius/Bundle/PayumBundle/Controller/PayumController.php @@ -23,7 +23,6 @@ use Payum\Core\Security\TokenInterface; use Sylius\Bundle\PayumBundle\Factory\GetStatusFactoryInterface; use Sylius\Bundle\PayumBundle\Factory\ResolveNextRouteFactoryInterface; use Sylius\Bundle\ResourceBundle\Controller\RequestConfigurationFactoryInterface; -use Sylius\Bundle\ResourceBundle\Controller\ViewHandlerInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; @@ -43,7 +42,6 @@ final class PayumController private OrderRepositoryInterface $orderRepository, private MetadataInterface $orderMetadata, private RequestConfigurationFactoryInterface $requestConfigurationFactory, - private ViewHandlerInterface $viewHandler, /** @phpstan-ignore-line */ private RouterInterface $router, private GetStatusFactoryInterface $getStatusRequestFactory, private ResolveNextRouteFactoryInterface $resolveNextRouteRequestFactory, diff --git a/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterPaypalGatewayTypePass.php b/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterPaypalGatewayTypePass.php new file mode 100644 index 0000000000..ca0b76b679 --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterPaypalGatewayTypePass.php @@ -0,0 +1,35 @@ +removeDefinition(self::PAYPAL_GATEWAY_TYPE_SERVICE_ID); + $container->removeDefinition(self::PAYPAL_CONVERT_ACTION_SERVICE_ID); + } +} diff --git a/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterStripeGatewayTypePass.php b/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterStripeGatewayTypePass.php index bb886e9fae..201ff3ed7e 100644 --- a/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterStripeGatewayTypePass.php +++ b/src/Sylius/Bundle/PayumBundle/DependencyInjection/Compiler/UnregisterStripeGatewayTypePass.php @@ -13,7 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\PayumBundle\DependencyInjection\Compiler; -use Stripe\Stripe; +use Payum\Stripe\StripeCheckoutGatewayFactory; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -21,9 +21,9 @@ final class UnregisterStripeGatewayTypePass implements CompilerPassInterface { private const STRIPE_GATEWAY_TYPE_SERVICE_ID = 'sylius.form.type.gateway_configuration.stripe'; - public function process(ContainerBuilder $container) + public function process(ContainerBuilder $container): void { - if (class_exists(Stripe::class)) { + if (class_exists(StripeCheckoutGatewayFactory::class)) { return; } diff --git a/src/Sylius/Bundle/PayumBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/PayumBundle/DependencyInjection/Configuration.php index 7e7ba1bd69..5cfee78a50 100644 --- a/src/Sylius/Bundle/PayumBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/PayumBundle/DependencyInjection/Configuration.php @@ -36,6 +36,15 @@ final class Configuration implements ConfigurationInterface $rootNode ->addDefaultsIfNotSet() ->children() + ->arrayNode('gateway_config') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->end() + ->end() ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() ->arrayNode('template') ->addDefaultsIfNotSet() diff --git a/src/Sylius/Bundle/PayumBundle/DependencyInjection/SyliusPayumExtension.php b/src/Sylius/Bundle/PayumBundle/DependencyInjection/SyliusPayumExtension.php index e7bce5b2f5..760306e14a 100644 --- a/src/Sylius/Bundle/PayumBundle/DependencyInjection/SyliusPayumExtension.php +++ b/src/Sylius/Bundle/PayumBundle/DependencyInjection/SyliusPayumExtension.php @@ -13,8 +13,10 @@ declare(strict_types=1); namespace Sylius\Bundle\PayumBundle\DependencyInjection; +use Sylius\Bundle\PayumBundle\Attribute\AsGatewayConfigurationType; use Sylius\Bundle\ResourceBundle\DependencyInjection\Extension\AbstractResourceExtension; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -25,13 +27,15 @@ final class SyliusPayumExtension extends AbstractResourceExtension implements Pr { $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); - $this->registerResources('sylius', $config['driver'], $config['resources'], $container); $loader->load('services.xml'); $container->setParameter('payum.template.layout', $config['template']['layout']); $container->setParameter('payum.template.obtain_credit_card', $config['template']['obtain_credit_card']); + $container->setParameter('sylius.payum.gateway_config.validation_groups', $config['gateway_config']['validation_groups']); + + $this->registerAutoconfiguration($container); } public function prepend(ContainerBuilder $container): void @@ -47,6 +51,7 @@ final class SyliusPayumExtension extends AbstractResourceExtension implements Pr continue; } + /** @var string $gatewayKey */ foreach (array_keys($config['gateways']) as $gatewayKey) { $gateways[$gatewayKey] = 'sylius.payum_gateway.' . $gatewayKey; } @@ -54,4 +59,18 @@ final class SyliusPayumExtension extends AbstractResourceExtension implements Pr $container->prependExtensionConfig('sylius_payment', ['gateways' => $gateways]); } + + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsGatewayConfigurationType::class, + static function (ChildDefinition $definition, AsGatewayConfigurationType $attribute): void { + $definition->addTag(AsGatewayConfigurationType::SERVICE_TAG, [ + 'type' => $attribute->getType(), + 'label' => $attribute->getLabel(), + 'priority' => $attribute->getPriority(), + ]); + }, + ); + } } diff --git a/src/Sylius/Bundle/PayumBundle/Extension/UpdatePaymentStateExtension.php b/src/Sylius/Bundle/PayumBundle/Extension/UpdatePaymentStateExtension.php index b9e8e5ffa1..73c216d70d 100644 --- a/src/Sylius/Bundle/PayumBundle/Extension/UpdatePaymentStateExtension.php +++ b/src/Sylius/Bundle/PayumBundle/Extension/UpdatePaymentStateExtension.php @@ -19,16 +19,25 @@ use Payum\Core\Request\Generic; use Payum\Core\Request\GetStatusInterface; use Payum\Core\Request\Notify; use SM\Factory\FactoryInterface; +use Sylius\Abstraction\StateMachine\StateMachineInterface; +use Sylius\Abstraction\StateMachine\WinzouStateMachineAdapter; use Sylius\Bundle\PayumBundle\Request\GetStatus; use Sylius\Component\Payment\Model\PaymentInterface; use Sylius\Component\Payment\PaymentTransitions; -use Sylius\Component\Resource\StateMachine\StateMachineInterface; -use Webmozart\Assert\Assert; final class UpdatePaymentStateExtension implements ExtensionInterface { - public function __construct(private FactoryInterface $factory) + public function __construct(private FactoryInterface|StateMachineInterface $factory) { + trigger_deprecation( + 'sylius/payum-bundle', + '1.13', + sprintf( + 'Passing an instance of "%s" as the first argument is deprecated. It will accept only instances of "%s" in Sylius 2.0.', + FactoryInterface::class, + StateMachineInterface::class, + ), + ); } public function onPreExecute(Context $context): void @@ -84,13 +93,19 @@ final class UpdatePaymentStateExtension implements ExtensionInterface private function updatePaymentState(PaymentInterface $payment, string $nextState): void { - $stateMachine = $this->factory->get($payment, PaymentTransitions::GRAPH); + $stateMachine = $this->getStateMachine(); - /** @var StateMachineInterface $stateMachine */ - Assert::isInstanceOf($stateMachine, StateMachineInterface::class); - - if (null !== $transition = $stateMachine->getTransitionToState($nextState)) { - $stateMachine->apply($transition); + if (null !== $transition = $stateMachine->getTransitionToState($payment, PaymentTransitions::GRAPH, $nextState)) { + $stateMachine->apply($payment, PaymentTransitions::GRAPH, $transition); } } + + private function getStateMachine(): StateMachineInterface + { + if ($this->factory instanceof FactoryInterface) { + return new WinzouStateMachineAdapter($this->factory); + } + + return $this->factory; + } } diff --git a/src/Sylius/Bundle/PayumBundle/Form/Extension/CryptedGatewayConfigTypeExtension.php b/src/Sylius/Bundle/PayumBundle/Form/Extension/CryptedGatewayConfigTypeExtension.php index 88e5d8d1a9..c9919f9d8c 100644 --- a/src/Sylius/Bundle/PayumBundle/Form/Extension/CryptedGatewayConfigTypeExtension.php +++ b/src/Sylius/Bundle/PayumBundle/Form/Extension/CryptedGatewayConfigTypeExtension.php @@ -58,11 +58,6 @@ final class CryptedGatewayConfigTypeExtension extends AbstractTypeExtension ; } - public function getExtendedType(): string - { - return GatewayConfigType::class; - } - public static function getExtendedTypes(): iterable { return [GatewayConfigType::class]; diff --git a/src/Sylius/Bundle/PayumBundle/Form/Type/PaypalGatewayConfigurationType.php b/src/Sylius/Bundle/PayumBundle/Form/Type/PaypalGatewayConfigurationType.php index 84e8e3a26d..919e4234ec 100644 --- a/src/Sylius/Bundle/PayumBundle/Form/Type/PaypalGatewayConfigurationType.php +++ b/src/Sylius/Bundle/PayumBundle/Form/Type/PaypalGatewayConfigurationType.php @@ -19,7 +19,6 @@ use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; -use Symfony\Component\Validator\Constraints\NotBlank; final class PaypalGatewayConfigurationType extends AbstractType { @@ -28,30 +27,12 @@ final class PaypalGatewayConfigurationType extends AbstractType $builder ->add('username', TextType::class, [ 'label' => 'sylius.form.gateway_configuration.paypal.username', - 'constraints' => [ - new NotBlank([ - 'message' => 'sylius.gateway_config.paypal.username.not_blank', - 'groups' => 'sylius', - ]), - ], ]) ->add('password', TextType::class, [ 'label' => 'sylius.form.gateway_configuration.paypal.password', - 'constraints' => [ - new NotBlank([ - 'message' => 'sylius.gateway_config.paypal.password.not_blank', - 'groups' => 'sylius', - ]), - ], ]) ->add('signature', TextType::class, [ 'label' => 'sylius.form.gateway_configuration.paypal.signature', - 'constraints' => [ - new NotBlank([ - 'message' => 'sylius.gateway_config.paypal.signature.not_blank', - 'groups' => 'sylius', - ]), - ], ]) ->add('sandbox', CheckboxType::class, [ 'label' => 'sylius.form.gateway_configuration.paypal.sandbox', diff --git a/src/Sylius/Bundle/PayumBundle/Form/Type/StripeGatewayConfigurationType.php b/src/Sylius/Bundle/PayumBundle/Form/Type/StripeGatewayConfigurationType.php index 447f9cb779..097bcff9a3 100644 --- a/src/Sylius/Bundle/PayumBundle/Form/Type/StripeGatewayConfigurationType.php +++ b/src/Sylius/Bundle/PayumBundle/Form/Type/StripeGatewayConfigurationType.php @@ -16,7 +16,6 @@ namespace Sylius\Bundle\PayumBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; final class StripeGatewayConfigurationType extends AbstractType { @@ -25,21 +24,9 @@ final class StripeGatewayConfigurationType extends AbstractType $builder ->add('publishable_key', TextType::class, [ 'label' => 'sylius.form.gateway_configuration.stripe.publishable_key', - 'constraints' => [ - new NotBlank([ - 'message' => 'sylius.gateway_config.stripe.publishable_key.not_blank', - 'groups' => 'sylius', - ]), - ], ]) ->add('secret_key', TextType::class, [ 'label' => 'sylius.form.gateway_configuration.stripe.secret_key', - 'constraints' => [ - new NotBlank([ - 'message' => 'sylius.gateway_config.stripe.secret_key.not_blank', - 'groups' => 'sylius', - ]), - ], ]) ; } diff --git a/src/Sylius/Bundle/PayumBundle/HttpClient/HttpClient.php b/src/Sylius/Bundle/PayumBundle/HttpClient/HttpClient.php new file mode 100644 index 0000000000..d38c9389cf --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/HttpClient/HttpClient.php @@ -0,0 +1,30 @@ +client->sendRequest($request); + } +} diff --git a/src/Sylius/Bundle/PayumBundle/Resources/config/services.xml b/src/Sylius/Bundle/PayumBundle/Resources/config/services.xml index ec493730eb..9061ed560f 100644 --- a/src/Sylius/Bundle/PayumBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/PayumBundle/Resources/config/services.xml @@ -17,21 +17,15 @@ http://symfony.com/schema/dic/services/services-1.0.xsd"> - - - - - - + - - + + - diff --git a/src/Sylius/Bundle/PayumBundle/Resources/config/services/controller.xml b/src/Sylius/Bundle/PayumBundle/Resources/config/services/controller.xml index f60f0f8645..6ef4fbee90 100644 --- a/src/Sylius/Bundle/PayumBundle/Resources/config/services/controller.xml +++ b/src/Sylius/Bundle/PayumBundle/Resources/config/services/controller.xml @@ -16,7 +16,6 @@ - diff --git a/src/Sylius/Bundle/PayumBundle/Resources/config/services/extension.xml b/src/Sylius/Bundle/PayumBundle/Resources/config/services/extension.xml index 145578cd38..3e0ab4f616 100644 --- a/src/Sylius/Bundle/PayumBundle/Resources/config/services/extension.xml +++ b/src/Sylius/Bundle/PayumBundle/Resources/config/services/extension.xml @@ -8,7 +8,7 @@ - + diff --git a/src/Sylius/Bundle/PayumBundle/Resources/config/services/validator.xml b/src/Sylius/Bundle/PayumBundle/Resources/config/services/validator.xml new file mode 100644 index 0000000000..150b0d3a0e --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/Resources/config/services/validator.xml @@ -0,0 +1,30 @@ + + + + + + + + + + %sylius.gateway_factories% + + + + + %sylius.payum.gateway_config.validation_groups% + + + diff --git a/src/Sylius/Bundle/PayumBundle/Resources/config/validation/GatewayConfig.xml b/src/Sylius/Bundle/PayumBundle/Resources/config/validation/GatewayConfig.xml new file mode 100644 index 0000000000..a5cc19b7a4 --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/Resources/config/validation/GatewayConfig.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/PayumBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/PayumBundle/Resources/translations/validators.en.yml index f833ea844b..c3ad59f7a2 100644 --- a/src/Sylius/Bundle/PayumBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/PayumBundle/Resources/translations/validators.en.yml @@ -1,5 +1,10 @@ sylius: gateway_config: + invalid_gateway_factory: Invalid gateway factory. Available factories are {{ available_factories }}. + gateway_name: + not_blank: Please enter gateway name. + factory_name: + not_blank: Please enter gateway factory name. paypal: password: not_blank: Please enter paypal password. @@ -7,6 +12,8 @@ sylius: not_blank: Please enter paypal signature. username: not_blank: Please enter paypal username. + sandbox: + not_null: Please set your paypal sandbox status. stripe: secret_key: not_blank: Please enter stripe secret key. diff --git a/src/Sylius/Bundle/PayumBundle/Resources/translations/validators.fr.yml b/src/Sylius/Bundle/PayumBundle/Resources/translations/validators.fr.yml index 1e03dbdb12..9fe29721ae 100644 --- a/src/Sylius/Bundle/PayumBundle/Resources/translations/validators.fr.yml +++ b/src/Sylius/Bundle/PayumBundle/Resources/translations/validators.fr.yml @@ -3,6 +3,11 @@ sylius: gateway_config: + invalid_gateway_factory: Usine de passerelle invalide. Les usines disponibles sont {{ available_factories }}. + gateway_name: + not_blank: Veuillez entrer le nom de la passerelle. + factory_name: + not_blank: Veuillez entrer le nom de l'usine de passerelle. paypal: password: not_blank: Veuillez entrer votre mot de passe PayPal. @@ -10,6 +15,8 @@ sylius: not_blank: Veuillez entrer la signature Paypal. username: not_blank: Veuillez entrer votre nom d'utilisateur PayPal. + sandbox: + not_null: Veuillez définir le statut de votre sandbox paypal. stripe: secret_key: not_blank: Veuillez entrer votre clé secrète Stripe. diff --git a/src/Sylius/Bundle/PayumBundle/SyliusPayumBundle.php b/src/Sylius/Bundle/PayumBundle/SyliusPayumBundle.php index 23a2bfe697..eb0601154c 100644 --- a/src/Sylius/Bundle/PayumBundle/SyliusPayumBundle.php +++ b/src/Sylius/Bundle/PayumBundle/SyliusPayumBundle.php @@ -15,6 +15,7 @@ namespace Sylius\Bundle\PayumBundle; use Sylius\Bundle\PayumBundle\DependencyInjection\Compiler\InjectContainerIntoControllersPass; use Sylius\Bundle\PayumBundle\DependencyInjection\Compiler\RegisterGatewayConfigTypePass; +use Sylius\Bundle\PayumBundle\DependencyInjection\Compiler\UnregisterPaypalGatewayTypePass; use Sylius\Bundle\PayumBundle\DependencyInjection\Compiler\UnregisterStripeGatewayTypePass; use Sylius\Bundle\PayumBundle\DependencyInjection\Compiler\UseTweakedDoctrineStoragePass; use Sylius\Bundle\ResourceBundle\AbstractResourceBundle; @@ -37,6 +38,7 @@ final class SyliusPayumBundle extends AbstractResourceBundle $container->addCompilerPass(new InjectContainerIntoControllersPass()); $container->addCompilerPass(new RegisterGatewayConfigTypePass()); $container->addCompilerPass(new UseTweakedDoctrineStoragePass()); + $container->addCompilerPass(new UnregisterPaypalGatewayTypePass(), priority: 128); $container->addCompilerPass(new UnregisterStripeGatewayTypePass(), priority: 128); } } diff --git a/src/Sylius/Bundle/PayumBundle/Tests/DependencyInjection/SyliusPayumExtensionTest.php b/src/Sylius/Bundle/PayumBundle/Tests/DependencyInjection/SyliusPayumExtensionTest.php new file mode 100644 index 0000000000..c83a69d468 --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/Tests/DependencyInjection/SyliusPayumExtensionTest.php @@ -0,0 +1,63 @@ +container->setDefinition( + 'acme.gateway_configuration_type_with_attribute', + (new Definition()) + ->setClass(GatewayConfigurationTypeStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.gateway_configuration_type_with_attribute', + AsGatewayConfigurationType::SERVICE_TAG, + ['type' => 'test', 'label' => 'Test', 'priority' => 15], + ); + } + + /** @test */ + public function it_loads_gateway_config_validation_groups_parameter_value_properly(): void + { + $this->load([ + 'gateway_config' => [ + 'validation_groups' => [ + 'paypal_express_checkout' => ['sylius', 'paypal'], + 'offline' => ['sylius'], + ], + ], + ]); + + $this->assertContainerBuilderHasParameter('sylius.payum.gateway_config.validation_groups', ['paypal_express_checkout' => ['sylius', 'paypal'], 'offline' => ['sylius']]); + } + + protected function getContainerExtensions(): array + { + return [new SyliusPayumExtension()]; + } +} diff --git a/src/Sylius/Bundle/PayumBundle/Tests/Stub/GatewayConfigurationTypeStub.php b/src/Sylius/Bundle/PayumBundle/Tests/Stub/GatewayConfigurationTypeStub.php new file mode 100644 index 0000000000..7141aebc4d --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/Tests/Stub/GatewayConfigurationTypeStub.php @@ -0,0 +1,21 @@ + $factoryNames */ + public function __construct(private array $factoryNames) + { + } + + /** @param string|null $value */ + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof GatewayFactoryExists) { + throw new UnexpectedTypeException($constraint, GatewayFactoryExists::class); + } + + if ($value === null || $value === '') { + return; + } + + if (!in_array($value, array_keys($this->factoryNames), true)) { + $this->context->buildViolation($constraint->invalidGatewayFactory) + ->setParameter('{{ available_factories }}', implode(', ', array_keys($this->factoryNames))) + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/PayumBundle/Validator/GroupsGenerator/GatewayConfigGroupsGenerator.php b/src/Sylius/Bundle/PayumBundle/Validator/GroupsGenerator/GatewayConfigGroupsGenerator.php new file mode 100644 index 0000000000..da2445f711 --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/Validator/GroupsGenerator/GatewayConfigGroupsGenerator.php @@ -0,0 +1,47 @@ +> $validationGroups */ + public function __construct(private array $validationGroups) + { + } + + /** + * @param FormInterface|PaymentMethodInterface $object + * + * @return array + */ + public function __invoke($object): array + { + if ($object instanceof FormInterface) { + $object = $object->getData(); + } + + Assert::isInstanceOf($object, PaymentMethodInterface::class); + + if ($object->getGatewayConfig()?->getFactoryName() === null) { + return ['sylius']; + } + + return $this->validationGroups[$object->getGatewayConfig()->getFactoryName()] ?? ['sylius']; + } +} diff --git a/src/Sylius/Bundle/PayumBundle/composer.json b/src/Sylius/Bundle/PayumBundle/composer.json index a5418df8b7..f16579da9b 100644 --- a/src/Sylius/Bundle/PayumBundle/composer.json +++ b/src/Sylius/Bundle/PayumBundle/composer.json @@ -28,19 +28,23 @@ } ], "require": { - "php": "^8.0", - "payum/payum": "^1.7.2", + "php": "^8.1", + "payum/offline": "^1.7.3", "payum/payum-bundle": "^2.5", - "php-http/guzzle6-adapter": "^2.0", - "sylius/core": "^1.12", - "sylius/currency": "^1.12", - "sylius/order-bundle": "^1.12", - "sylius/payment-bundle": "^1.12", - "sylius/resource-bundle": "^1.9" + "sylius/core": "^1.13", + "sylius/currency": "^1.13", + "sylius/order-bundle": "^1.13", + "sylius/payment-bundle": "^1.13", + "sylius/resource-bundle": "^1.9", + "php-http/httplug": "^2.4" }, "require-dev": { "phpspec/phpspec": "^7.2", - "symfony/dependency-injection": "^5.4 || ^6.0" + "symfony/dependency-injection": "^5.4.21 || ^6.4" + }, + "suggest": { + "payum/paypal-express-checkout-nvp": "To use PayPal Express Chekout NVP", + "payum/stripe": "To use Stripe Checkout" }, "config": { "allow-plugins": { @@ -49,7 +53,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { @@ -59,7 +63,8 @@ }, "autoload-dev": { "psr-4": { - "Sylius\\Bundle\\PayumBundle\\spec\\": "spec/" + "Sylius\\Bundle\\PayumBundle\\spec\\": "spec/", + "Sylius\\Bundle\\PayumBundle\\Tests\\": "Tests/" } }, "repositories": [ diff --git a/src/Sylius/Bundle/PayumBundle/spec/HttpClient/HttpClientSpec.php b/src/Sylius/Bundle/PayumBundle/spec/HttpClient/HttpClientSpec.php new file mode 100644 index 0000000000..dab8046bf1 --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/spec/HttpClient/HttpClientSpec.php @@ -0,0 +1,49 @@ +beConstructedWith($client); + } + + function it_is_initializable(): void + { + $this->shouldHaveType(HttpClient::class); + } + + function it_implements_http_client_interface(): void + { + $this->shouldImplement(HttpClientInterface::class); + } + + function it_sends_a_request( + ClientInterface $client, + RequestInterface $request, + ResponseInterface $response, + ): void { + $client->sendRequest($request)->willReturn($response); + + $this->send($request)->shouldReturn($response); + } +} diff --git a/src/Sylius/Bundle/PayumBundle/spec/Validator/GatewayFactoryExistsValidatorSpec.php b/src/Sylius/Bundle/PayumBundle/spec/Validator/GatewayFactoryExistsValidatorSpec.php new file mode 100644 index 0000000000..bce09b164a --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/spec/Validator/GatewayFactoryExistsValidatorSpec.php @@ -0,0 +1,65 @@ +beConstructedWith( + ['paypal_express_checkout' => 'sylius.payum_gateway_factory.paypal_express_checkout', 'stripe_checkout' => 'sylius.payum_gateway_factory.stripe_checkout'], + ); + + $this->initialize($executionContext); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_gateway_factory_exists( + Constraint $constraint, + GatewayConfigInterface $gatewayConfig, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$gatewayConfig, $constraint]) + ; + } + + function it_adds_violation_to_gateway_configuration_with_wrong_name( + ExecutionContextInterface $executionContext, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ): void { + $executionContext->buildViolation((new GatewayFactoryExists())->invalidGatewayFactory)->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->shouldBeCalled()->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + + $this->validate('wrong_factory', new GatewayFactoryExists()); + } + + function it_does_not_add_violation_to_gateway_configuration_with_correct_name( + ExecutionContextInterface $executionContext, + ): void { + $executionContext->buildViolation(Argument::cetera())->shouldNotBeCalled(); + + $this->validate('paypal_express_checkout', new GatewayFactoryExists()); + } +} diff --git a/src/Sylius/Bundle/PayumBundle/spec/Validator/GroupsGenerator/GatewayConfigGroupsGeneratorSpec.php b/src/Sylius/Bundle/PayumBundle/spec/Validator/GroupsGenerator/GatewayConfigGroupsGeneratorSpec.php new file mode 100644 index 0000000000..c7915fdb23 --- /dev/null +++ b/src/Sylius/Bundle/PayumBundle/spec/Validator/GroupsGenerator/GatewayConfigGroupsGeneratorSpec.php @@ -0,0 +1,68 @@ +beConstructedWith([ + 'paypal_express_checkout' => ['paypal_express_checkout', 'sylius'], + 'stripe_checkout' => ['stripe_checkout', 'sylius'], + ]); + } + + function it_throws_error_if_invalid_object_is_passed(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new \stdClass()]) + ; + } + + function it_returns_gateway_config_validation_groups( + GatewayConfigInterface $gatewayConfig, + PaymentMethodInterface $paymentMethod, + ): void { + $paymentMethod->getGatewayConfig()->willReturn($gatewayConfig); + $gatewayConfig->getFactoryName()->willReturn('paypal_express_checkout'); + + $this($paymentMethod)->shouldReturn(['paypal_express_checkout', 'sylius']); + } + + function it_returns_default_validation_groups_if_gateway_config_is_null( + PaymentMethodInterface $paymentMethod, + ): void { + $paymentMethod->getGatewayConfig()->willReturn(null); + + $this($paymentMethod)->shouldReturn(['sylius']); + } + + function it_returns_gateway_config_validation_groups_if_it_is_payment_method_form( + FormInterface $form, + GatewayConfigInterface $gatewayConfig, + PaymentMethodInterface $paymentMethod, + ): void { + $form->getData()->willReturn($paymentMethod); + $paymentMethod->getGatewayConfig()->willReturn($gatewayConfig); + $gatewayConfig->getFactoryName()->willReturn('paypal_express_checkout'); + + $this($form)->shouldReturn(['paypal_express_checkout', 'sylius']); + } +} diff --git a/src/Sylius/Bundle/ProductBundle/Attribute/AsProductVariantResolver.php b/src/Sylius/Bundle/ProductBundle/Attribute/AsProductVariantResolver.php new file mode 100644 index 0000000000..56a03ecc07 --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/Attribute/AsProductVariantResolver.php @@ -0,0 +1,29 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/ProductBundle/Controller/ProductSlugController.php b/src/Sylius/Bundle/ProductBundle/Controller/ProductSlugController.php index 92cf79f563..752d5ba340 100644 --- a/src/Sylius/Bundle/ProductBundle/Controller/ProductSlugController.php +++ b/src/Sylius/Bundle/ProductBundle/Controller/ProductSlugController.php @@ -24,7 +24,12 @@ class ProductSlugController extends AbstractController public function __construct(private ?SlugGeneratorInterface $slugGenerator = null) { if ($this->slugGenerator === null) { - @trigger_error(sprintf('Not passing a $slugGenerator to %s constructor is deprecated since Sylius 1.11 and will be prohibited in Sylius 2.0.', self::class), \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/product-bundle', + '1.11', + 'Not passing a $slugGenerator to %s constructor is deprecated and will be prohibited in Sylius 2.0.', + self::class, + ); } } diff --git a/src/Sylius/Bundle/ProductBundle/DependencyInjection/Compiler/DefaultProductVariantResolverCompilerPass.php b/src/Sylius/Bundle/ProductBundle/DependencyInjection/Compiler/DefaultProductVariantResolverCompilerPass.php new file mode 100644 index 0000000000..e1617494cd --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/DependencyInjection/Compiler/DefaultProductVariantResolverCompilerPass.php @@ -0,0 +1,35 @@ +has('sylius.product_variant_resolver.default')) { + return; + } + + // In case someone overwritten the service, we need to make sure it's tagged + $defaultResolver = $container->getDefinition('sylius.product_variant_resolver.default'); + if (!$defaultResolver->hasTag('sylius.product_variant_resolver')) { + $defaultResolver->addTag('sylius.product_variant_resolver', ['priority' => self::DEFAULT_PRIORITY]); + } + } +} diff --git a/src/Sylius/Bundle/ProductBundle/DependencyInjection/SyliusProductExtension.php b/src/Sylius/Bundle/ProductBundle/DependencyInjection/SyliusProductExtension.php index c54345147e..3e0e460f5f 100644 --- a/src/Sylius/Bundle/ProductBundle/DependencyInjection/SyliusProductExtension.php +++ b/src/Sylius/Bundle/ProductBundle/DependencyInjection/SyliusProductExtension.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace Sylius\Bundle\ProductBundle\DependencyInjection; +use Sylius\Bundle\ProductBundle\Attribute\AsProductVariantResolver; use Sylius\Bundle\ProductBundle\Controller\ProductAttributeController; use Sylius\Bundle\ProductBundle\Doctrine\ORM\ProductAttributeValueRepository; use Sylius\Bundle\ProductBundle\Form\Type\ProductAttributeTranslationType; @@ -26,6 +27,7 @@ use Sylius\Component\Product\Model\ProductAttributeTranslationInterface; use Sylius\Component\Product\Model\ProductAttributeValue; use Sylius\Component\Product\Model\ProductAttributeValueInterface; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -42,6 +44,8 @@ final class SyliusProductExtension extends AbstractResourceExtension implements $this->registerResources('sylius', $config['driver'], $config['resources'], $container); $loader->load('services.xml'); + + $this->registerAutoconfiguration($container); } public function prepend(ContainerBuilder $container): void @@ -51,6 +55,16 @@ final class SyliusProductExtension extends AbstractResourceExtension implements $this->prependAttribute($container, $config); } + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsProductVariantResolver::class, + static function (ChildDefinition $definition, AsProductVariantResolver $attribute): void { + $definition->addTag(AsProductVariantResolver::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + } + private function prependAttribute(ContainerBuilder $container, array $config): void { if (!$container->hasExtension('sylius_attribute')) { diff --git a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductAssociationTypeRepository.php b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductAssociationTypeRepository.php index 5600c96b2f..e806e8442f 100644 --- a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductAssociationTypeRepository.php +++ b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductAssociationTypeRepository.php @@ -15,8 +15,14 @@ namespace Sylius\Bundle\ProductBundle\Doctrine\ORM; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Product\Model\ProductAssociationTypeInterface; use Sylius\Component\Product\Repository\ProductAssociationTypeRepositoryInterface; +/** + * @template T of ProductAssociationTypeInterface + * + * @implements ProductAssociationTypeRepositoryInterface + */ class ProductAssociationTypeRepository extends EntityRepository implements ProductAssociationTypeRepositoryInterface { public function createListQueryBuilder(string $locale): QueryBuilder diff --git a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductAttributeValueRepository.php b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductAttributeValueRepository.php index a825bedc6a..f52883dd9c 100644 --- a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductAttributeValueRepository.php +++ b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductAttributeValueRepository.php @@ -15,8 +15,14 @@ namespace Sylius\Bundle\ProductBundle\Doctrine\ORM; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Product\Model\ProductAttributeValueInterface; use Sylius\Component\Product\Repository\ProductAttributeValueRepositoryInterface; +/** + * @template T of ProductAttributeValueInterface + * + * @implements ProductAttributeValueRepositoryInterface + */ class ProductAttributeValueRepository extends EntityRepository implements ProductAttributeValueRepositoryInterface { public function findByJsonChoiceKey(string $choiceKey): array diff --git a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductOptionRepository.php b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductOptionRepository.php index f019736a65..cbb6707c54 100644 --- a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductOptionRepository.php +++ b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductOptionRepository.php @@ -18,6 +18,11 @@ use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Product\Model\ProductOptionInterface; use Sylius\Component\Product\Repository\ProductOptionRepositoryInterface; +/** + * @template T of ProductOptionInterface + * + * @implements ProductOptionRepositoryInterface + */ class ProductOptionRepository extends EntityRepository implements ProductOptionRepositoryInterface { public function createListQueryBuilder(string $locale): QueryBuilder diff --git a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductRepository.php b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductRepository.php index dd24b8828a..d9a395efbf 100644 --- a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductRepository.php +++ b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductRepository.php @@ -14,8 +14,14 @@ declare(strict_types=1); namespace Sylius\Bundle\ProductBundle\Doctrine\ORM; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Product\Model\ProductInterface; use Sylius\Component\Product\Repository\ProductRepositoryInterface; +/** + * @template T of ProductInterface + * + * @implements ProductRepositoryInterface + */ class ProductRepository extends EntityRepository implements ProductRepositoryInterface { public function findByName(string $name, string $locale): array diff --git a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductVariantRepository.php b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductVariantRepository.php index 63d790d9ef..4341298ef5 100644 --- a/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductVariantRepository.php +++ b/src/Sylius/Bundle/ProductBundle/Doctrine/ORM/ProductVariantRepository.php @@ -19,6 +19,11 @@ use Sylius\Component\Product\Model\ProductInterface; use Sylius\Component\Product\Model\ProductVariantInterface; use Sylius\Component\Product\Repository\ProductVariantRepositoryInterface; +/** + * @template T of ProductVariantInterface + * + * @implements ProductVariantRepositoryInterface + */ class ProductVariantRepository extends EntityRepository implements ProductVariantRepositoryInterface { public function createQueryBuilderByProductId(string $locale, $productId): QueryBuilder diff --git a/src/Sylius/Bundle/ProductBundle/Form/DataTransformer/ProductsToProductAssociationsTransformer.php b/src/Sylius/Bundle/ProductBundle/Form/DataTransformer/ProductsToProductAssociationsTransformer.php index 1b80027298..6ee25e2bc7 100644 --- a/src/Sylius/Bundle/ProductBundle/Form/DataTransformer/ProductsToProductAssociationsTransformer.php +++ b/src/Sylius/Bundle/ProductBundle/Form/DataTransformer/ProductsToProductAssociationsTransformer.php @@ -80,6 +80,9 @@ final class ProductsToProductAssociationsTransformer implements DataTransformerI return $productAssociations; } + /** + * @param Collection $products + */ private function getCodesAsStringFromProducts(Collection $products): ?string { if ($products->isEmpty()) { diff --git a/src/Sylius/Bundle/ProductBundle/Form/EventSubscriber/BuildAttributesFormSubscriber.php b/src/Sylius/Bundle/ProductBundle/Form/EventSubscriber/BuildAttributesFormSubscriber.php index f22a44f536..5861f1d53d 100644 --- a/src/Sylius/Bundle/ProductBundle/Form/EventSubscriber/BuildAttributesFormSubscriber.php +++ b/src/Sylius/Bundle/ProductBundle/Form/EventSubscriber/BuildAttributesFormSubscriber.php @@ -53,7 +53,7 @@ final class BuildAttributesFormSubscriber implements EventSubscriberInterface $defaultLocaleCode = $this->localeProvider->getDefaultLocaleCode(); $attributes = $product->getAttributes()->filter( - fn (ProductAttributeValueInterface $attribute) => $attribute->getLocaleCode() === $defaultLocaleCode, + fn (AttributeValueInterface $attribute) => $attribute->getLocaleCode() === $defaultLocaleCode, ); /** @var ProductAttributeValueInterface $attribute */ diff --git a/src/Sylius/Bundle/ProductBundle/Form/EventSubscriber/GenerateProductVariantsSubscriber.php b/src/Sylius/Bundle/ProductBundle/Form/EventSubscriber/GenerateProductVariantsSubscriber.php index f9938b1ab7..3fc3f1163a 100644 --- a/src/Sylius/Bundle/ProductBundle/Form/EventSubscriber/GenerateProductVariantsSubscriber.php +++ b/src/Sylius/Bundle/ProductBundle/Form/EventSubscriber/GenerateProductVariantsSubscriber.php @@ -28,10 +28,17 @@ final class GenerateProductVariantsSubscriber implements EventSubscriberInterfac { public function __construct( private ProductVariantGeneratorInterface $generator, - private SessionInterface|RequestStack $requestStackOrSession, + private RequestStack|SessionInterface $requestStackOrSession, ) { if ($requestStackOrSession instanceof SessionInterface) { - @trigger_error(sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', SessionInterface::class, self::class, RequestStack::class), \E_USER_DEPRECATED); + trigger_deprecation( + 'sylius/product-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + SessionInterface::class, + self::class, + RequestStack::class, + ); } } diff --git a/src/Sylius/Bundle/ProductBundle/Form/Type/ProductOptionChoiceType.php b/src/Sylius/Bundle/ProductBundle/Form/Type/ProductOptionChoiceType.php index c7932f5018..00e165ec24 100644 --- a/src/Sylius/Bundle/ProductBundle/Form/Type/ProductOptionChoiceType.php +++ b/src/Sylius/Bundle/ProductBundle/Form/Type/ProductOptionChoiceType.php @@ -21,10 +21,19 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; +/** + * @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. + */ final class ProductOptionChoiceType extends AbstractType { public function __construct(private RepositoryInterface $productOptionRepository) { + trigger_deprecation( + 'sylius/product-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0.', + self::class, + ); } public function buildForm(FormBuilderInterface $builder, array $options): void diff --git a/src/Sylius/Bundle/ProductBundle/Form/Type/ProductOptionValueChoiceType.php b/src/Sylius/Bundle/ProductBundle/Form/Type/ProductOptionValueChoiceType.php index f41bfefc44..486291abe6 100644 --- a/src/Sylius/Bundle/ProductBundle/Form/Type/ProductOptionValueChoiceType.php +++ b/src/Sylius/Bundle/ProductBundle/Form/Type/ProductOptionValueChoiceType.php @@ -28,9 +28,10 @@ final class ProductOptionValueChoiceType extends AbstractType public function __construct(?AvailableProductOptionValuesResolverInterface $availableProductOptionValuesResolver = null) { if (null === $availableProductOptionValuesResolver) { - @trigger_error( - 'Not passing availableProductOptionValuesResolver thru constructor is deprecated in Sylius 1.8 and ' . - 'it will be removed in Sylius 2.0', + trigger_deprecation( + 'sylius/product-bundle', + '1.8', + 'Not passing an $availableProductOptionValuesResolver through constructor is deprecated and will be removed in Sylius 2.0', ); } diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/doctrine/model/ProductAttributeValue.orm.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/doctrine/model/ProductAttributeValue.orm.xml index a0da0bcb42..2ff76dbf58 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/doctrine/model/ProductAttributeValue.orm.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/doctrine/model/ProductAttributeValue.orm.xml @@ -16,6 +16,14 @@ xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd"> - + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/services.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/services.xml index d208556321..aff7cfa492 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/services.xml @@ -13,7 +13,7 @@ - + @@ -37,15 +37,6 @@ - - - - - - - - - @@ -58,9 +49,13 @@ + + + + - + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/services/form.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/services/form.xml index 52989569f1..9fa032bc38 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/services/form.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/services/form.xml @@ -121,13 +121,14 @@ + %sylius.model.product.class% %sylius.form.type.product.validation_groups% - + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/services/validators.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/services/validators.xml new file mode 100644 index 0000000000..8eaa9610cc --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/services/validators.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociation.xml new file mode 100644 index 0000000000..538cac1f4d --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociation.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociationTypeTranslation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociationTypeTranslation.xml index 26a3620b74..df4f749856 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociationTypeTranslation.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductAssociationTypeTranslation.xml @@ -13,6 +13,14 @@ + + + + + @@ -26,5 +34,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionTranslation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionTranslation.xml index 737c9b2df9..187d665cf8 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionTranslation.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionTranslation.xml @@ -13,6 +13,14 @@ + + + + + @@ -26,5 +34,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionValueTranslation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionValueTranslation.xml index 9a78fa6a91..b1319877de 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionValueTranslation.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductOptionValueTranslation.xml @@ -13,11 +13,29 @@ + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductTranslation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductTranslation.xml index c3b92078e2..2f4df4de3c 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductTranslation.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductTranslation.xml @@ -13,6 +13,14 @@ + + + + + @@ -60,5 +68,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariant.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariant.xml index b25ea1af75..027933f9cc 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariant.xml +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariant.xml @@ -18,6 +18,13 @@ + + + + + + + @@ -29,8 +36,13 @@ - - - + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariantTranslation.xml b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariantTranslation.xml new file mode 100644 index 0000000000..7129cc580d --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/Resources/config/validation/ProductVariantTranslation.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml index d13341caa5..8e90f61fcb 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.en.yml @@ -26,6 +26,8 @@ sylius: regex: Product variant code can only be comprised of letters, numbers, dashes and underscores. unique: Product variant code must be unique. within_product_unique: This code must be unique within this product. + option_values: + not_configured: 'The product variant must have configured values for all options chosen on the product.' simple_product: code: unique: Simple product code must be unique among all products and product variants. @@ -47,6 +49,12 @@ sylius: unique: The option value with given code already exists. value: not_blank: Please enter option value. + association: + unique: An association with this owner and type already exists. + type: + not_blank: Please enter association type. + owner: + not_blank: Please enter association owner. association_type: name: not_blank: Please enter association type name. @@ -56,3 +64,8 @@ sylius: not_blank: Please enter association type code. regex: Association type code can only be comprised of letters, numbers, dashes and underscores. unique: The association type with given code already exists. + translation: + locale: + not_blank: Please enter the locale. + invalid: This value is not a valid locale. + unique: A translation for the {{ value }} locale code already exists. diff --git a/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.fr.yml b/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.fr.yml index 2e6144fe76..7a0cc59244 100644 --- a/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.fr.yml +++ b/src/Sylius/Bundle/ProductBundle/Resources/translations/validators.fr.yml @@ -29,6 +29,8 @@ sylius: regex: Le code de la variante du produit peut uniquement être constitué de lettres, chiffres, tirets et tirets bas. unique: Le code de la variante du produit doit être unique. within_product_unique: Ce code doit être unique au sein de ce produit. + option_values: + not_configured: 'La variante du produit doit avoir des valeurs configurées pour toutes les options choisies sur le produit.' simple_product: code: unique: Le code d'un simple produit doit être unique parmi tous les produits et les variantes de produit. @@ -50,6 +52,12 @@ sylius: unique: La valeur de l'option existe déjà. value: not_blank: Veuillez saisir la valeur de l'option. + association: + unique: Une association avec ce propriétaire et ce type existe déjà. + type: + not_blank: Veuillez entrer un type d'association. + owner: + not_blank: Veuillez entrer le propriétaire de l'association. association_type: name: not_blank: Entrez le nom du type d'association. @@ -59,3 +67,8 @@ sylius: not_blank: Veuillez entrer le code du type d'association. regex: Le code du type d'association peut uniquement être constitué de lettres, chiffres, tirets et tirets bas. unique: Le type d'association avec le code donné existe déjà. + translation: + locale: + not_blank: Vous devez entrer une valeur. + invalid: Ce paramètre régional n'est pas valide. + unique: Une traduction pour la locale {{ value }} existe déjà. diff --git a/src/Sylius/Bundle/ProductBundle/SyliusProductBundle.php b/src/Sylius/Bundle/ProductBundle/SyliusProductBundle.php index 8f4af32e73..0425b84083 100644 --- a/src/Sylius/Bundle/ProductBundle/SyliusProductBundle.php +++ b/src/Sylius/Bundle/ProductBundle/SyliusProductBundle.php @@ -13,11 +13,20 @@ declare(strict_types=1); namespace Sylius\Bundle\ProductBundle; +use Sylius\Bundle\ProductBundle\DependencyInjection\Compiler\DefaultProductVariantResolverCompilerPass; use Sylius\Bundle\ResourceBundle\AbstractResourceBundle; use Sylius\Bundle\ResourceBundle\SyliusResourceBundle; +use Symfony\Component\DependencyInjection\ContainerBuilder; final class SyliusProductBundle extends AbstractResourceBundle { + public function build(ContainerBuilder $container): void + { + parent::build($container); + + $container->addCompilerPass(new DefaultProductVariantResolverCompilerPass()); + } + public function getSupportedDrivers(): array { return [ diff --git a/src/Sylius/Bundle/ProductBundle/Tests/DependencyInjection/Compiler/DefaultProductVariantResolverCompilerPassTest.php b/src/Sylius/Bundle/ProductBundle/Tests/DependencyInjection/Compiler/DefaultProductVariantResolverCompilerPassTest.php new file mode 100644 index 0000000000..c8d4a5fdcc --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/Tests/DependencyInjection/Compiler/DefaultProductVariantResolverCompilerPassTest.php @@ -0,0 +1,67 @@ +compile(); + + $this->assertContainerBuilderNotHasService('sylius.product_variant_resolver.default'); + } + + /** @test */ + public function it_does_nothing_when_default_resolver_already_has_the_tag(): void + { + $defaultResolver = $this->registerService( + 'sylius.product_variant_resolver.default', + ProductVariantResolverInterface::class, + ); + $defaultResolver->addTag('sylius.product_variant_resolver'); + + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'sylius.product_variant_resolver.default', + 'sylius.product_variant_resolver', + ); + } + + /** @test */ + public function it_adds_variant_resolver_tag_with_very_low_priority_to_the_default_resolver(): void + { + $this->setDefinition('sylius.product_variant_resolver.default', new Definition()); + + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'sylius.product_variant_resolver.default', + 'sylius.product_variant_resolver', + ['priority' => -999], + ); + } + + protected function registerCompilerPass(ContainerBuilder $container): void + { + $container->addCompilerPass(new DefaultProductVariantResolverCompilerPass()); + } +} diff --git a/src/Sylius/Bundle/ProductBundle/Tests/DependencyInjection/SyliusProductExtensionsTest.php b/src/Sylius/Bundle/ProductBundle/Tests/DependencyInjection/SyliusProductExtensionsTest.php new file mode 100644 index 0000000000..2ac0c7fcbd --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/Tests/DependencyInjection/SyliusProductExtensionsTest.php @@ -0,0 +1,47 @@ +container->setDefinition( + 'acme.product_variant_resolver_autoconfigured', + (new Definition()) + ->setClass(ProductVariantResolverStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.product_variant_resolver_autoconfigured', + 'sylius.product_variant_resolver', + ['priority' => 50], + ); + } + + protected function getContainerExtensions(): array + { + return [new SyliusProductExtension()]; + } +} diff --git a/src/Sylius/Bundle/ProductBundle/Tests/Stub/ProductVariantResolverStub.php b/src/Sylius/Bundle/ProductBundle/Tests/Stub/ProductVariantResolverStub.php new file mode 100644 index 0000000000..666232b4e6 --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/Tests/Stub/ProductVariantResolverStub.php @@ -0,0 +1,28 @@ +getProduct(); - if (!$product->hasVariants() || !$product->hasOptions()) { + + if ($product === null || !$product->hasVariants() || !$product->hasOptions()) { return; } diff --git a/src/Sylius/Bundle/ProductBundle/Validator/ProductVariantOptionValuesConfigurationValidator.php b/src/Sylius/Bundle/ProductBundle/Validator/ProductVariantOptionValuesConfigurationValidator.php new file mode 100644 index 0000000000..4dfb909ece --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/Validator/ProductVariantOptionValuesConfigurationValidator.php @@ -0,0 +1,52 @@ +getProduct(); + if ($product === null || !$product->hasOptions()) { + return; + } + + $requiredOptionCodes = array_map( + fn (ProductOptionInterface $productOption) => $productOption->getCode(), + $product->getOptions()->toArray(), + ); + $variantOptionCodes = array_map( + fn (ProductOptionValueInterface $productOptionValue) => $productOptionValue->getOptionCode(), + $value->getOptionValues()->toArray(), + ); + + if (!empty(array_diff($requiredOptionCodes, $variantOptionCodes))) { + $this->context->addViolation($constraint->message); + } + } +} diff --git a/src/Sylius/Bundle/ProductBundle/Validator/UniqueSimpleProductCodeValidator.php b/src/Sylius/Bundle/ProductBundle/Validator/UniqueSimpleProductCodeValidator.php index dbc3375197..96252abc1e 100644 --- a/src/Sylius/Bundle/ProductBundle/Validator/UniqueSimpleProductCodeValidator.php +++ b/src/Sylius/Bundle/ProductBundle/Validator/UniqueSimpleProductCodeValidator.php @@ -28,7 +28,7 @@ final class UniqueSimpleProductCodeValidator extends ConstraintValidator { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var UniqueSimpleProductCode $constraint */ Assert::isInstanceOf($constraint, UniqueSimpleProductCode::class); diff --git a/src/Sylius/Bundle/ProductBundle/composer.json b/src/Sylius/Bundle/ProductBundle/composer.json index bf923eafb7..a259d2a3b7 100644 --- a/src/Sylius/Bundle/ProductBundle/composer.json +++ b/src/Sylius/Bundle/ProductBundle/composer.json @@ -26,23 +26,24 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "stof/doctrine-extensions-bundle": "^1.4", - "sylius/attribute-bundle": "^1.12", - "sylius/locale-bundle": "^1.12", - "sylius/product": "^1.12", + "sylius/attribute-bundle": "^1.13", + "sylius/locale-bundle": "^1.13", + "sylius/product": "^1.13", "sylius/resource-bundle": "^1.9", - "symfony/framework-bundle": "^5.4 || ^6.0" + "symfony/framework-bundle": "^5.4.21 || ^6.4" }, "require-dev": { "doctrine/orm": "^2.13", + "matthiasnoback/symfony-dependency-injection-test": "^4.2", "mockery/mockery": "^1.4", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0", - "symfony/validator": "^5.4 || ^6.0" + "phpunit/phpunit": "^9.5", + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4", + "symfony/validator": "^5.4.21 || ^6.4" }, "conflict": { "doctrine/orm": "2.15.2", @@ -55,7 +56,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { diff --git a/src/Sylius/Bundle/ProductBundle/spec/Validator/ProductVariantCombinationValidatorSpec.php b/src/Sylius/Bundle/ProductBundle/spec/Validator/ProductVariantCombinationValidatorSpec.php index b983da9237..b8341510f3 100644 --- a/src/Sylius/Bundle/ProductBundle/spec/Validator/ProductVariantCombinationValidatorSpec.php +++ b/src/Sylius/Bundle/ProductBundle/spec/Validator/ProductVariantCombinationValidatorSpec.php @@ -27,6 +27,7 @@ final class ProductVariantCombinationValidatorSpec extends ObjectBehavior function let(ExecutionContextInterface $context, ProductVariantsParityCheckerInterface $variantsParityChecker): void { $this->beConstructedWith($variantsParityChecker); + $this->initialize($context); } @@ -35,6 +36,28 @@ final class ProductVariantCombinationValidatorSpec extends ObjectBehavior $this->shouldImplement(ConstraintValidator::class); } + function it_does_not_add_violation_if_product_is_null( + ExecutionContextInterface $context, + ProductInterface $product, + ProductVariantInterface $variant, + ProductVariantsParityCheckerInterface $variantsParityChecker, + ): void { + $constraint = new ProductVariantCombination([ + 'message' => 'Variant with given options already exists', + ]); + + $variant->getProduct()->willReturn(null); + + $product->hasVariants()->shouldNotBeCalled(); + $product->hasOptions()->shouldNotBeCalled(); + + $variantsParityChecker->checkParity($variant, $product)->shouldNotBeCalled(); + + $context->addViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate($variant, $constraint); + } + function it_does_not_add_violation_if_product_does_not_have_options( ExecutionContextInterface $context, ProductInterface $product, @@ -50,7 +73,7 @@ final class ProductVariantCombinationValidatorSpec extends ObjectBehavior $product->hasVariants()->willReturn(true); $product->hasOptions()->willReturn(false); - $variantsParityChecker->checkParity($variant, $product)->willReturn(false); + $variantsParityChecker->checkParity($variant, $product)->shouldNotBeCalled(); $context->addViolation(Argument::any())->shouldNotBeCalled(); @@ -74,7 +97,7 @@ final class ProductVariantCombinationValidatorSpec extends ObjectBehavior $context->addViolation(Argument::any())->shouldNotBeCalled(); - $variantsParityChecker->checkParity($variant, $product)->willReturn(false); + $variantsParityChecker->checkParity($variant, $product)->shouldNotBeCalled(); $this->validate($variant, $constraint); } diff --git a/src/Sylius/Bundle/ProductBundle/spec/Validator/ProductVariantOptionValuesConfigurationValidatorSpec.php b/src/Sylius/Bundle/ProductBundle/spec/Validator/ProductVariantOptionValuesConfigurationValidatorSpec.php new file mode 100644 index 0000000000..69c4b8572f --- /dev/null +++ b/src/Sylius/Bundle/ProductBundle/spec/Validator/ProductVariantOptionValuesConfigurationValidatorSpec.php @@ -0,0 +1,152 @@ +initialize($executionContext); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldImplement(ConstraintValidatorInterface::class); + } + + function it_throws_an_exception_if_value_is_not_a_product_variant( + ExecutionContextInterface $context, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [new \stdClass(), new ProductVariantOptionValuesConfiguration()]) + ; + } + + function it_throws_an_exception_if_constraint_is_not_a_product_variant_option_values_configuration( + ExecutionContextInterface $context, + ProductVariantInterface $variant, + Constraint $constraint, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('validate', [$variant, $constraint]) + ; + } + + function it_adds_violation_if_not_all_options_have_configured_values_on_the_variant( + ExecutionContextInterface $executionContext, + ProductVariantInterface $variant, + ProductInterface $product, + ProductOptionInterface $firstOption, + ProductOptionInterface $secondOption, + ProductOptionValueInterface $optionValue, + ): void { + $constraint = new ProductVariantOptionValuesConfiguration(); + + $variant->getProduct()->willReturn($product); + $product->hasOptions()->willReturn(true); + + $firstOption->getCode()->willReturn('SIZE'); + $secondOption->getCode()->willReturn('COLOUR'); + $product->getOptions()->willReturn(new ArrayCollection([ + $firstOption->getWrappedObject(), + $secondOption->getWrappedObject(), + ])); + + $optionValue->getOptionCode()->willReturn('SIZE'); + $variant->getOptionValues()->willReturn(new ArrayCollection([$optionValue->getWrappedObject()])); + + $executionContext->addViolation($constraint->message)->shouldBeCalled(); + + $this->validate($variant, $constraint); + } + + function it_does_nothing_if_all_options_have_configured_values_on_the_variant( + ExecutionContextInterface $executionContext, + ProductVariantInterface $variant, + ProductInterface $product, + ProductOptionInterface $firstOption, + ProductOptionInterface $secondOption, + ProductOptionValueInterface $firstProductOptionValue, + ProductOptionValueInterface $secondProductOptionValue, + ): void { + $constraint = new ProductVariantOptionValuesConfiguration(); + + $variant->getProduct()->willReturn($product); + $product->hasOptions()->willReturn(true); + + $firstOption->getCode()->willReturn('SIZE'); + $secondOption->getCode()->willReturn('COLOUR'); + $product->getOptions()->willReturn(new ArrayCollection([ + $firstOption->getWrappedObject(), + $secondOption->getWrappedObject(), + ])); + + $firstProductOptionValue->getOptionCode()->willReturn('SIZE'); + $secondProductOptionValue->getOptionCode()->willReturn('COLOUR'); + $variant->getOptionValues()->willReturn(new ArrayCollection([ + $firstProductOptionValue->getWrappedObject(), + $secondProductOptionValue->getWrappedObject(), + ])); + + $executionContext->addViolation($constraint->message)->shouldNotBeCalled(); + + $this->validate($variant, $constraint); + } + + function it_does_nothing_if_variant_does_not_have_product( + ExecutionContextInterface $executionContext, + ProductVariantInterface $variant, + ): void { + $constraint = new ProductVariantOptionValuesConfiguration(); + + $variant->getProduct()->willReturn(null); + + $executionContext->addViolation($constraint->message)->shouldNotBeCalled(); + + $this->validate($variant, $constraint); + } + + function it_does_nothing_if_product_does_not_have_options( + ExecutionContextInterface $executionContext, + ProductVariantInterface $variant, + ProductInterface $product, + ): void { + $constraint = new ProductVariantOptionValuesConfiguration(); + + $variant->getProduct()->willReturn($product); + $product->hasOptions()->willReturn(false); + + $executionContext->addViolation($constraint->message)->shouldNotBeCalled(); + + $this->validate($variant, $constraint); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionAction.php b/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionAction.php new file mode 100644 index 0000000000..0e3db3bd4b --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionAction.php @@ -0,0 +1,48 @@ +type; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getFormType(): string + { + return $this->formType; + } + + public function getPriority(): int + { + return $this->priority; + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionCouponEligibilityChecker.php b/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionCouponEligibilityChecker.php new file mode 100644 index 0000000000..487843a62a --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionCouponEligibilityChecker.php @@ -0,0 +1,29 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionEligibilityChecker.php b/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionEligibilityChecker.php new file mode 100644 index 0000000000..e65768b041 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionEligibilityChecker.php @@ -0,0 +1,29 @@ +priority; + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionRuleChecker.php b/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionRuleChecker.php new file mode 100644 index 0000000000..06e04a11df --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Attribute/AsPromotionRuleChecker.php @@ -0,0 +1,48 @@ +type; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getFormType(): string + { + return $this->formType; + } + + public function getPriority(): int + { + return $this->priority; + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php b/src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php index 5a3b78b07c..6e3a30c127 100644 --- a/src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php +++ b/src/Sylius/Bundle/PromotionBundle/Command/GenerateCouponsCommand.php @@ -13,82 +13,18 @@ declare(strict_types=1); namespace Sylius\Bundle\PromotionBundle\Command; -use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstruction; -use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstructionInterface; -use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInterface; -use Sylius\Component\Promotion\Model\PromotionInterface; -use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; +trigger_deprecation( + 'sylius/promotion-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + GenerateCouponsCommand::class, + \Sylius\Bundle\PromotionBundle\Console\Command\GenerateCouponsCommand::class, +); -final class GenerateCouponsCommand extends Command -{ - protected static $defaultName = 'sylius:promotion:generate-coupons'; +class_exists(\Sylius\Bundle\PromotionBundle\Console\Command\GenerateCouponsCommand::class); - public function __construct( - private PromotionRepositoryInterface $promotionRepository, - private PromotionCouponGeneratorInterface $couponGenerator, - ) { - parent::__construct(); - } - - protected function configure(): void +if (false) { + final class GenerateCouponsCommand { - $this - ->setDescription('Generates coupons for a given promotion') - ->addArgument('promotion-code', InputArgument::REQUIRED, 'Code of the promotion') - ->addArgument('count', InputArgument::REQUIRED, 'Amount of coupons to generate') - ->addOption('length', 'len', InputOption::VALUE_OPTIONAL, 'Length of the coupon code (default 10)', '10') - ; - } - - public function execute(InputInterface $input, OutputInterface $output): int - { - /** @var string $promotionCode */ - $promotionCode = $input->getArgument('promotion-code'); - - /** @var PromotionInterface|null $promotion */ - $promotion = $this->promotionRepository->findOneBy(['code' => $promotionCode]); - - if ($promotion === null) { - $output->writeln('No promotion found with this code'); - - return 1; - } - - if (!$promotion->isCouponBased()) { - $output->writeln('This promotion is not coupon based'); - - return 1; - } - - $instruction = $this->getGeneratorInstructions( - (int) $input->getArgument('count'), - (int) $input->getOption('length'), - ); - - try { - $this->couponGenerator->generate($promotion, $instruction); - } catch (\Exception $exception) { - $output->writeln('' . $exception->getMessage() . ''); - - return 1; - } - - $output->writeln('Coupons have been generated'); - - return 0; - } - - public function getGeneratorInstructions(int $count, int $codeLength): PromotionCouponGeneratorInstructionInterface - { - $instruction = new PromotionCouponGeneratorInstruction(); - $instruction->setAmount($count); - $instruction->setCodeLength($codeLength); - - return $instruction; } } diff --git a/src/Sylius/Bundle/PromotionBundle/Console/Command/GenerateCouponsCommand.php b/src/Sylius/Bundle/PromotionBundle/Console/Command/GenerateCouponsCommand.php new file mode 100644 index 0000000000..22e4cc21dc --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Console/Command/GenerateCouponsCommand.php @@ -0,0 +1,96 @@ + $promotionRepository */ + public function __construct( + private PromotionRepositoryInterface $promotionRepository, + private PromotionCouponGeneratorInterface $couponGenerator, + ) { + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setDescription('Generates coupons for a given promotion') + ->addArgument('promotion-code', InputArgument::REQUIRED, 'Code of the promotion') + ->addArgument('count', InputArgument::REQUIRED, 'Amount of coupons to generate') + ->addOption('length', 'len', InputOption::VALUE_OPTIONAL, 'Length of the coupon code (default 10)', '10') + ; + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + /** @var string $promotionCode */ + $promotionCode = $input->getArgument('promotion-code'); + + /** @var PromotionInterface|null $promotion */ + $promotion = $this->promotionRepository->findOneBy(['code' => $promotionCode]); + + if ($promotion === null) { + $output->writeln('No promotion found with this code'); + + return 1; + } + + if (!$promotion->isCouponBased()) { + $output->writeln('This promotion is not coupon based'); + + return 1; + } + + $instruction = $this->getGeneratorInstructions( + (int) $input->getArgument('count'), + (int) $input->getOption('length'), + ); + + try { + $this->couponGenerator->generate($promotion, $instruction); + } catch (\Exception $exception) { + $output->writeln('' . $exception->getMessage() . ''); + + return 1; + } + + $output->writeln('Coupons have been generated'); + + return 0; + } + + public function getGeneratorInstructions(int $count, int $codeLength): ReadablePromotionCouponGeneratorInstructionInterface + { + return new PromotionCouponGeneratorInstruction( + amount: $count, + codeLength: $codeLength, + ); + } +} + +class_alias(GenerateCouponsCommand::class, \Sylius\Bundle\PromotionBundle\Command\GenerateCouponsCommand::class); diff --git a/src/Sylius/Bundle/PromotionBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/PromotionBundle/DependencyInjection/Configuration.php index 415a7a83a8..9f3934b202 100644 --- a/src/Sylius/Bundle/PromotionBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/PromotionBundle/DependencyInjection/Configuration.php @@ -19,6 +19,7 @@ use Sylius\Bundle\PromotionBundle\Form\Type\CatalogPromotionType; use Sylius\Bundle\PromotionBundle\Form\Type\PromotionActionType; use Sylius\Bundle\PromotionBundle\Form\Type\PromotionCouponType; use Sylius\Bundle\PromotionBundle\Form\Type\PromotionRuleType; +use Sylius\Bundle\PromotionBundle\Form\Type\PromotionTranslationType; use Sylius\Bundle\PromotionBundle\Form\Type\PromotionType; use Sylius\Bundle\ResourceBundle\Controller\ResourceController; use Sylius\Bundle\ResourceBundle\Form\Type\DefaultResourceType; @@ -39,6 +40,8 @@ use Sylius\Component\Promotion\Model\PromotionCouponInterface; use Sylius\Component\Promotion\Model\PromotionInterface; use Sylius\Component\Promotion\Model\PromotionRule; use Sylius\Component\Promotion\Model\PromotionRuleInterface; +use Sylius\Component\Promotion\Model\PromotionTranslation; +use Sylius\Component\Promotion\Model\PromotionTranslationInterface; use Sylius\Component\Resource\Factory\Factory; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; @@ -55,7 +58,43 @@ final class Configuration implements ConfigurationInterface $rootNode ->addDefaultsIfNotSet() ->children() - ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() + ->arrayNode('promotion_action') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->end() + ->end() + ->arrayNode('promotion_rule') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->end() + ->end() + ->arrayNode('catalog_promotion_action') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->end() + ->end() + ->arrayNode('catalog_promotion_scope') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->end() + ->end() + ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() ->end() ; @@ -99,6 +138,23 @@ final class Configuration implements ConfigurationInterface ->scalarNode('form')->defaultValue(PromotionType::class)->cannotBeEmpty()->end() ->end() ->end() + ->arrayNode('translation') + ->addDefaultsIfNotSet() + ->children() + ->variableNode('options')->end() + ->arrayNode('classes') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('model')->defaultValue(PromotionTranslation::class)->cannotBeEmpty()->end() + ->scalarNode('interface')->defaultValue(PromotionTranslationInterface::class)->cannotBeEmpty()->end() + ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() + ->scalarNode('repository')->cannotBeEmpty()->end() + ->scalarNode('factory')->defaultValue(Factory::class)->end() + ->scalarNode('form')->defaultValue(PromotionTranslationType::class)->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() ->end() ->end() ->arrayNode('catalog_promotion') diff --git a/src/Sylius/Bundle/PromotionBundle/DependencyInjection/SyliusPromotionExtension.php b/src/Sylius/Bundle/PromotionBundle/DependencyInjection/SyliusPromotionExtension.php index 83f326e34a..1166d73281 100644 --- a/src/Sylius/Bundle/PromotionBundle/DependencyInjection/SyliusPromotionExtension.php +++ b/src/Sylius/Bundle/PromotionBundle/DependencyInjection/SyliusPromotionExtension.php @@ -13,8 +13,13 @@ declare(strict_types=1); namespace Sylius\Bundle\PromotionBundle\DependencyInjection; +use Sylius\Bundle\PromotionBundle\Attribute\AsPromotionAction; +use Sylius\Bundle\PromotionBundle\Attribute\AsPromotionCouponEligibilityChecker; +use Sylius\Bundle\PromotionBundle\Attribute\AsPromotionEligibilityChecker; +use Sylius\Bundle\PromotionBundle\Attribute\AsPromotionRuleChecker; use Sylius\Bundle\ResourceBundle\DependencyInjection\Extension\AbstractResourceExtension; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -29,5 +34,57 @@ final class SyliusPromotionExtension extends AbstractResourceExtension $loader->load(sprintf('services/integrations/%s.xml', $config['driver'])); $this->registerResources('sylius', $config['driver'], $config['resources'], $container); + $this->registerValidationParameters($container, $config); + $this->registerAutoconfiguration($container); + } + + /** @param array>> $configuration */ + private function registerValidationParameters(ContainerBuilder $container, array $configuration): void + { + $container->setParameter('sylius.promotion.promotion_action.validation_groups', $configuration['promotion_action']['validation_groups']); + $container->setParameter('sylius.promotion.promotion_rule.validation_groups', $configuration['promotion_rule']['validation_groups']); + $container->setParameter('sylius.promotion.catalog_promotion_action.validation_groups', $configuration['catalog_promotion_action']['validation_groups']); + $container->setParameter('sylius.promotion.catalog_promotion_scope.validation_groups', $configuration['catalog_promotion_scope']['validation_groups']); + } + + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsPromotionAction::class, + static function (ChildDefinition $definition, AsPromotionAction $attribute): void { + $definition->addTag(AsPromotionAction::SERVICE_TAG, [ + 'type' => $attribute->getType(), + 'label' => $attribute->getLabel(), + 'form-type' => $attribute->getFormType(), + 'priority' => $attribute->getPriority(), + ]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsPromotionCouponEligibilityChecker::class, + static function (ChildDefinition $definition, AsPromotionCouponEligibilityChecker $attribute): void { + $definition->addTag(AsPromotionCouponEligibilityChecker::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsPromotionEligibilityChecker::class, + static function (ChildDefinition $definition, AsPromotionEligibilityChecker $attribute): void { + $definition->addTag(AsPromotionEligibilityChecker::SERVICE_TAG, ['priority' => $attribute->getPriority()]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsPromotionRuleChecker::class, + static function (ChildDefinition $definition, AsPromotionRuleChecker $attribute): void { + $definition->addTag(AsPromotionRuleChecker::SERVICE_TAG, [ + 'type' => $attribute->getType(), + 'label' => $attribute->getLabel(), + 'form-type' => $attribute->getFormType(), + 'priority' => $attribute->getPriority(), + ]); + }, + ); } } diff --git a/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/CatalogPromotionRepository.php b/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/CatalogPromotionRepository.php index 953f2dc8ea..5a2cb7b2f1 100644 --- a/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/CatalogPromotionRepository.php +++ b/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/CatalogPromotionRepository.php @@ -15,8 +15,14 @@ namespace Sylius\Bundle\PromotionBundle\Doctrine\ORM; use Sylius\Bundle\PromotionBundle\Criteria\CriteriaInterface; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Promotion\Model\CatalogPromotionInterface; use Sylius\Component\Promotion\Repository\CatalogPromotionRepositoryInterface; +/** + * @template T of CatalogPromotionInterface + * + * @implements CatalogPromotionRepositoryInterface + */ class CatalogPromotionRepository extends EntityRepository implements CatalogPromotionRepositoryInterface { public function findByCriteria(iterable $criteria): array diff --git a/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/PromotionCouponRepository.php b/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/PromotionCouponRepository.php index c00b7bef2d..793e87e30b 100644 --- a/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/PromotionCouponRepository.php +++ b/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/PromotionCouponRepository.php @@ -18,6 +18,11 @@ use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; use Sylius\Component\Promotion\Model\PromotionCouponInterface; use Sylius\Component\Promotion\Repository\PromotionCouponRepositoryInterface; +/** + * @template T of PromotionCouponInterface + * + * @implements PromotionCouponRepositoryInterface + */ class PromotionCouponRepository extends EntityRepository implements PromotionCouponRepositoryInterface { public function createQueryBuilderByPromotionId($promotionId): QueryBuilder diff --git a/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/PromotionRepository.php b/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/PromotionRepository.php index f5043708a8..93d9c8ecc2 100644 --- a/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/PromotionRepository.php +++ b/src/Sylius/Bundle/PromotionBundle/Doctrine/ORM/PromotionRepository.php @@ -15,8 +15,14 @@ namespace Sylius\Bundle\PromotionBundle\Doctrine\ORM; use Doctrine\ORM\QueryBuilder; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Promotion\Model\PromotionInterface; use Sylius\Component\Promotion\Repository\PromotionRepositoryInterface; +/** + * @template T of PromotionInterface + * + * @implements PromotionRepositoryInterface + */ class PromotionRepository extends EntityRepository implements PromotionRepositoryInterface { public function findActive(): array diff --git a/src/Sylius/Bundle/PromotionBundle/Form/DataTransformer/MoneyIntToLocalizedStringTransformer.php b/src/Sylius/Bundle/PromotionBundle/Form/DataTransformer/MoneyIntToLocalizedStringTransformer.php index 78014e815d..e3f91ac311 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/DataTransformer/MoneyIntToLocalizedStringTransformer.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/DataTransformer/MoneyIntToLocalizedStringTransformer.php @@ -17,7 +17,7 @@ use Symfony\Component\Form\Extension\Core\DataTransformer\MoneyToLocalizedString final class MoneyIntToLocalizedStringTransformer extends MoneyToLocalizedStringTransformer { - public function reverseTransform(mixed $value): int|float|null + public function reverseTransform(mixed $value): float|int|null { if (!is_numeric($value)) { return null; diff --git a/src/Sylius/Bundle/PromotionBundle/Form/DataTransformer/PercentFloatToLocalizedStringTransformer.php b/src/Sylius/Bundle/PromotionBundle/Form/DataTransformer/PercentFloatToLocalizedStringTransformer.php index 1ecd418b54..55d2ef10b8 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/DataTransformer/PercentFloatToLocalizedStringTransformer.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/DataTransformer/PercentFloatToLocalizedStringTransformer.php @@ -28,7 +28,7 @@ final class PercentFloatToLocalizedStringTransformer extends PercentToLocalizedS * @throws TransformationFailedException if the given value is not a string or * if the value could not be transformed */ - public function reverseTransform(mixed $value): int|float|null + public function reverseTransform(mixed $value): float|int|null { if (!is_numeric($value)) { return null; diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/FixedDiscountConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/FixedDiscountConfigurationType.php index 3db54c124d..acbbb7645f 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/FixedDiscountConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/FixedDiscountConfigurationType.php @@ -17,8 +17,6 @@ use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class FixedDiscountConfigurationType extends AbstractType { @@ -27,10 +25,6 @@ final class FixedDiscountConfigurationType extends AbstractType $builder ->add('amount', MoneyType::class, [ 'label' => 'sylius.form.promotion_action.fixed_discount_configuration.amount', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - ], 'currency' => $options['currency'], ]) ; diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/PercentageDiscountConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/PercentageDiscountConfigurationType.php index c7730401dc..5cb5f630ee 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/PercentageDiscountConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/PercentageDiscountConfigurationType.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\PromotionBundle\Form\Type\Action; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Type; final class PercentageDiscountConfigurationType extends AbstractType { @@ -29,16 +26,6 @@ final class PercentageDiscountConfigurationType extends AbstractType 'label' => 'sylius.form.promotion_action.percentage_discount_configuration.percentage', 'html5' => true, 'scale' => 2, - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - new Range([ - 'min' => 0, - 'max' => 1, - 'notInRangeMessage' => 'sylius.promotion_action.percentage_discount_configuration.not_in_range', - 'groups' => ['sylius'], - ]), - ], ]) ; } diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitFixedDiscountConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitFixedDiscountConfigurationType.php index 84d3c61bf1..7bd7b11fa5 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitFixedDiscountConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitFixedDiscountConfigurationType.php @@ -18,8 +18,6 @@ use Sylius\Bundle\PromotionBundle\Form\Type\PromotionFilterCollectionType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class UnitFixedDiscountConfigurationType extends AbstractType { @@ -28,14 +26,10 @@ final class UnitFixedDiscountConfigurationType extends AbstractType $builder ->add('amount', MoneyType::class, [ 'label' => 'sylius.form.promotion_action.fixed_discount_configuration.amount', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - ], 'currency' => $options['currency'], ]) ->add('filters', PromotionFilterCollectionType::class, [ - 'label' => false, + 'label' => 'sylius.form.promotion_action.filters', 'required' => false, 'currency' => $options['currency'], ]) diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitPercentageDiscountConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitPercentageDiscountConfigurationType.php index de28338b86..29f605cb32 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitPercentageDiscountConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Action/UnitPercentageDiscountConfigurationType.php @@ -18,9 +18,6 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Type; final class UnitPercentageDiscountConfigurationType extends AbstractType { @@ -31,18 +28,9 @@ final class UnitPercentageDiscountConfigurationType extends AbstractType 'label' => 'sylius.form.promotion_action.percentage_discount_configuration.percentage', 'html5' => true, 'scale' => 2, - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - new Range([ - 'min' => 0, - 'max' => 1, - 'notInRangeMessage' => 'sylius.promotion_action.percentage_discount_configuration.not_in_range', - 'groups' => ['sylius'], - ]), - ], ]) ->add('filters', PromotionFilterCollectionType::class, [ + 'label' => 'sylius.form.promotion_action.filters', 'required' => false, 'currency' => $options['currency'], ]) diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionAction/FixedDiscountActionConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionAction/FixedDiscountActionConfigurationType.php index ed5c7f496a..8a135c09bf 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionAction/FixedDiscountActionConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionAction/FixedDiscountActionConfigurationType.php @@ -17,7 +17,6 @@ use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\Type; final class FixedDiscountActionConfigurationType extends AbstractType { @@ -26,9 +25,6 @@ final class FixedDiscountActionConfigurationType extends AbstractType $builder ->add('amount', MoneyType::class, [ 'label' => 'sylius.ui.amount', - 'constraints' => [ - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - ], 'currency' => $options['currency'], ]) ; diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionAction/PercentageDiscountActionConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionAction/PercentageDiscountActionConfigurationType.php index e060fef513..e35aa0705f 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionAction/PercentageDiscountActionConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/CatalogPromotionAction/PercentageDiscountActionConfigurationType.php @@ -16,8 +16,6 @@ namespace Sylius\Bundle\PromotionBundle\Form\Type\CatalogPromotionAction; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Range; final class PercentageDiscountActionConfigurationType extends AbstractType { @@ -28,18 +26,6 @@ final class PercentageDiscountActionConfigurationType extends AbstractType 'label' => 'sylius.ui.amount', 'html5' => true, 'scale' => 2, - 'constraints' => [ - new NotBlank([ - 'groups' => 'sylius', - 'message' => 'sylius.catalog_promotion_action.percentage_discount.not_number_or_empty', - ]), - new Range([ - 'min' => 0, - 'max' => 1, - 'notInRangeMessage' => 'sylius.catalog_promotion_action.percentage_discount.not_in_range', - 'groups' => ['sylius'], - ]), - ], ]) ; } diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php index 8a7046307f..4e3b9bf52b 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionCouponGeneratorInstructionType.php @@ -13,14 +13,26 @@ declare(strict_types=1); namespace Sylius\Bundle\PromotionBundle\Form\Type; -use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; +use Sylius\Component\Promotion\Factory\PromotionCouponGeneratorInstructionFactoryInterface; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\DataMapperInterface; use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; -final class PromotionCouponGeneratorInstructionType extends AbstractResourceType +final class PromotionCouponGeneratorInstructionType extends AbstractType implements DataMapperInterface { + /** @param array $validationGroups */ + public function __construct( + private DataMapperInterface $propertyPathDataMapper, + private PromotionCouponGeneratorInstructionFactoryInterface $promotionCouponGeneratorInstructionFactory, + private string $dataClass, + private array $validationGroups = [], + ) { + } + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder @@ -47,9 +59,33 @@ final class PromotionCouponGeneratorInstructionType extends AbstractResourceType 'label' => 'sylius.form.promotion_coupon_generator_instruction.expires_at', 'widget' => 'single_text', ]) + ->setDataMapper($this) ; } + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => $this->dataClass, + 'validation_groups' => $this->validationGroups, + ]); + } + + public function mapDataToForms($viewData, \Traversable $forms): void + { + $this->propertyPathDataMapper->mapDataToForms($viewData, $forms); + } + + public function mapFormsToData(\Traversable $forms, &$viewData): void + { + $data = []; + foreach ($forms as $form) { + $data[$form->getName()] = $form->getData(); + } + + $viewData = $this->promotionCouponGeneratorInstructionFactory->createFromArray($data); + } + public function getBlockPrefix(): string { return 'sylius_promotion_coupon_generator_instruction'; diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionTranslationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionTranslationType.php new file mode 100644 index 0000000000..a7e458150c --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionTranslationType.php @@ -0,0 +1,36 @@ +add('label', TextType::class, [ + 'label' => 'sylius.form.promotion.label', + 'required' => false, + ]) + ; + } + + public function getBlockPrefix(): string + { + return 'sylius_promotion_translation'; + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionType.php index 11977bfb18..931976bba2 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/PromotionType.php @@ -15,6 +15,7 @@ namespace Sylius\Bundle\PromotionBundle\Form\Type; use Sylius\Bundle\ResourceBundle\Form\EventSubscriber\AddCodeFormSubscriber; use Sylius\Bundle\ResourceBundle\Form\Type\AbstractResourceType; +use Sylius\Bundle\ResourceBundle\Form\Type\ResourceTranslationsType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; @@ -24,12 +25,24 @@ use Symfony\Component\Form\FormBuilderInterface; final class PromotionType extends AbstractResourceType { + public function __construct( + string $dataClass, + array $validationGroups, + private string $promotionTranslationType, + ) { + parent::__construct($dataClass, $validationGroups); + } + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('name', TextType::class, [ 'label' => 'sylius.form.promotion.name', ]) + ->add('translations', ResourceTranslationsType::class, [ + 'entry_type' => $this->promotionTranslationType, + 'label' => 'sylius.form.promotion.translations', + ]) ->add('description', TextareaType::class, [ 'label' => 'sylius.form.promotion.description', 'required' => false, diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/CartQuantityConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/CartQuantityConfigurationType.php index 26d653f0f8..670764d5ff 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/CartQuantityConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/CartQuantityConfigurationType.php @@ -16,8 +16,6 @@ namespace Sylius\Bundle\PromotionBundle\Form\Type\Rule; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class CartQuantityConfigurationType extends AbstractType { @@ -26,10 +24,6 @@ final class CartQuantityConfigurationType extends AbstractType $builder ->add('count', IntegerType::class, [ 'label' => 'sylius.form.promotion_rule.cart_quantity_configuration.count', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - ], ]) ; } diff --git a/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/ItemTotalConfigurationType.php b/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/ItemTotalConfigurationType.php index 5387ba96d2..7c9137b5dc 100644 --- a/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/ItemTotalConfigurationType.php +++ b/src/Sylius/Bundle/PromotionBundle/Form/Type/Rule/ItemTotalConfigurationType.php @@ -17,8 +17,6 @@ use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class ItemTotalConfigurationType extends AbstractType { @@ -27,10 +25,6 @@ final class ItemTotalConfigurationType extends AbstractType $builder ->add('amount', MoneyType::class, [ 'label' => 'sylius.form.promotion_rule.item_total_configuration.amount', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - ], 'currency' => $options['currency'], ]) ; diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/PromotionBundle/Resources/config/app/config.yml index 06622bebee..a6793e8ace 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/app/config.yml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/app/config.yml @@ -7,3 +7,19 @@ jms_serializer: sylius-promotion: namespace_prefix: "Sylius\\Component\\Promotion" path: "@SyliusPromotionBundle/Resources/config/serializer" + +sylius_promotion: + promotion_rule: + validation_groups: + cart_quantity: + - 'sylius' + - 'sylius_promotion_rule_cart_quantity' + item_total: + - 'sylius' + - 'sylius_promotion_rule_item_total' + + catalog_promotion_action: + validation_groups: + percentage_discount: + - 'sylius' + - 'sylius_catalog_promotion_action_percentage_discount' diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/Promotion.orm.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/Promotion.orm.xml index b4d1ef3b87..7a54f32412 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/Promotion.orm.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/Promotion.orm.xml @@ -34,6 +34,7 @@ + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/PromotionTranslation.orm.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/PromotionTranslation.orm.xml new file mode 100644 index 0000000000..c1fbbded50 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/doctrine/model/PromotionTranslation.orm.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services.xml index 54d7ad2bf5..a1c70e4eb2 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services.xml @@ -13,11 +13,7 @@ - - - - - + @@ -28,6 +24,9 @@ + + + @@ -37,6 +36,7 @@ + The "%service_id%" service is deprecated since 1.13 and will be removed in 2.0. diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/command.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/console_command.xml similarity index 70% rename from src/Sylius/Bundle/PromotionBundle/Resources/config/services/command.xml rename to src/Sylius/Bundle/PromotionBundle/Resources/config/services/console_command.xml index dee44b7b64..f17f6eb853 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/command.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/console_command.xml @@ -2,10 +2,12 @@ - + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/eligibility_checkers.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/eligibility_checkers.xml index 62b13a7ca8..caf2fed7e3 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/eligibility_checkers.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/eligibility_checkers.xml @@ -55,5 +55,10 @@ class="Sylius\Component\Promotion\Checker\Eligibility\CompositePromotionEligibilityChecker"> + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml index 1cf98aed1a..cb329e3b03 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/forms.xml @@ -22,6 +22,9 @@ sylius + + sylius + sylius @@ -31,6 +34,9 @@ sylius + + sylius + @@ -71,6 +77,13 @@ %sylius.model.promotion.class% %sylius.form.type.promotion.validation_groups% + Sylius\Bundle\PromotionBundle\Form\Type\PromotionTranslationType + + + + + %sylius.model.promotion_translation.class% + %sylius.form.type.promotion_translation.validation_groups% @@ -118,8 +131,14 @@ + + + + Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstruction + %sylius.form.type.promotion_coupon_generator_instruction.validation_groups% + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/validators.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/validators.xml index 3e6f62d404..724792ad72 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/services/validators.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/services/validators.xml @@ -35,10 +35,54 @@ + + %sylius.promotion.catalog_promotion_action.validation_groups% + + + + + %sylius.catalog_promotion.actions_types% + + + %sylius.catalog_promotion.scopes_types% + + + %sylius.promotion.catalog_promotion_scope.validation_groups% + + + + + %sylius.catalog_promotion.scopes_types% + + + + + %sylius.promotion.promotion_action.validation_groups% + + + + + %sylius.promotion_actions% + + + + + %sylius.promotion.promotion_rule.validation_groups% + + + + + %sylius.promotion_rules% + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotion.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotion.xml index 37674a788c..4249c16f82 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotion.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotion.xml @@ -62,5 +62,8 @@ + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionAction.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionAction.xml index 18ea9412a9..754fe24a55 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionAction.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionAction.xml @@ -13,8 +13,44 @@ - + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionScope.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionScope.xml new file mode 100644 index 0000000000..d2f89e87d5 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionScope.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionTranslation.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionTranslation.xml new file mode 100644 index 0000000000..ac0ee147b3 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/CatalogPromotionTranslation.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/Promotion.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/Promotion.xml index 66e19ef88b..8ecad21bfd 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/Promotion.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/Promotion.xml @@ -61,5 +61,8 @@ + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionAction.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionAction.xml index 8707311c99..47b3d1cc43 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionAction.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionAction.xml @@ -13,8 +13,11 @@ - - - + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCoupon.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCoupon.xml index 91ed8fd982..5b906d06b2 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCoupon.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCoupon.xml @@ -18,6 +18,16 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml index 67cedc3a11..fbe8ec1068 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionCouponGeneratorInstruction.xml @@ -13,20 +13,25 @@ - + + + + + + @@ -34,9 +39,11 @@ + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionRule.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionRule.xml index 3b3186767c..70a721f539 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionRule.xml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionRule.xml @@ -13,8 +13,11 @@ - - - + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionTranslation.xml b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionTranslation.xml new file mode 100644 index 0000000000..599500ebd5 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Resources/config/validation/PromotionTranslation.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.en.yml b/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.en.yml index c57e3bde11..d7225d4934 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.en.yml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.en.yml @@ -24,6 +24,7 @@ sylius: description: Description ends_at: Ends at exclusive: Exclusive + label: Label name: Name priority: Priority rules: Rules @@ -35,6 +36,7 @@ sylius: order_fixed_discount: Order fixed discount order_percentage_discount: Order percentage discount shipping_percentage_discount: Shipping percentage discount + filters: Filters fixed_discount_configuration: amount: Amount percentage_discount_configuration: diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.fr.yml b/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.fr.yml index 79c6a7208e..a87f6696fe 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.fr.yml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/translations/messages.fr.yml @@ -25,6 +25,7 @@ sylius: description: Description ends_at: Se termine le exclusive: Exclusif + label: Libellé name: Nom priority: Priorité rules: Règles @@ -36,6 +37,7 @@ sylius: order_fixed_discount: Remise sur la commande order_percentage_discount: Rabais en pourcentage commande shipping_percentage_discount: Rabais en pourcentage expédition + filters: Filtres fixed_discount_configuration: amount: Montant percentage_discount_configuration: diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml index 5fdb11feac..4e9ee7c0d1 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.en.yml @@ -16,11 +16,18 @@ sylius: state: processing: The catalog promotion cannot be edited as it is currently being processed. catalog_promotion_action: + type: + not_blank: Please choose an action type. + invalid: Catalog promotion action type is invalid. Available types are {{ available_action_types }}. invalid_type: Catalog promotion action type is invalid. Please choose a valid type. percentage_discount: not_in_range: The percentage discount amount must be between 0% and 100%. not_number_or_empty: The percentage discount amount must be a number and can not be empty. not_valid: The percentage discount amount must be configured. + catalog_promotion_scope: + type: + not_blank: Please choose a scope type. + invalid: Catalog promotion scope type is invalid. Available types are {{ available_scope_types }}. promotion: code: unique: The promotion with given code already exists. @@ -37,8 +44,11 @@ sylius: min_length: Promotion name must be at least 1 character long.|Promotion name must be at least {{ limit }} characters long. not_blank: Please enter promotion name. promotion_action: + invalid_type: Promotion action type is invalid. Available action types are {{ available_action_types }}. percentage_discount_configuration: not_in_range: The percentage discount must be between 0% and 100%. + promotion_rule: + invalid_type: Promotion rule type is invalid. Available rule types are {{ available_rule_types }}. promotion_coupon: code: max_length: Coupon code must not be longer than 1 character.|Coupon code must not be longer than {{ limit }} characters. @@ -47,6 +57,9 @@ sylius: regex: Coupon code can only be comprised of letters, numbers, dashes and underscores. unique: This coupon already exists. is_invalid: Coupon code is invalid. + promotion: + not_blank: Please provide a promotion for this coupon. + not_coupon_based: Only coupon based promotions can have coupons. usage_limit: min: Coupon usage limit must be at least {{ limit }}. promotion_coupon_generator_instruction: @@ -61,3 +74,8 @@ sylius: possible_generation_amount: Invalid coupons code length or coupons amount. It is not possible to generate %expectedAmount% unique coupons with code length %codeLength%. The possible amount to generate is %possibleAmount%. usage_limit: min: Usage limit of generated coupons must be at least {{ limit }}. + translation: + locale: + not_blank: Please enter the locale. + invalid: This value is not a valid locale. + unique: A translation for the {{ value }} locale code already exists. diff --git a/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.fr.yml b/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.fr.yml index 2267813f39..b89c318b48 100644 --- a/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.fr.yml +++ b/src/Sylius/Bundle/PromotionBundle/Resources/translations/validators.fr.yml @@ -16,11 +16,18 @@ sylius: state: processing: La promotion du catalogue ne peut pas être modifiée car elle est en cours de traitement. catalog_promotion_action: + type: + not_blank: Veuillez choisir un type d'action. + invalid: Le type d'action de promotion catalogue n'est pas valide. Les types disponibles sont {{ available_action_types }}. invalid_type: Le type d'action de la promotion catalogue n'est pas valide. Veuillez choisir un type d'action valide. percentage_discount: not_in_range: Le montant de la remise en pourcentage doit être compris entre 0% et 100%. not_number_or_empty: Le montant de la remise en pourcentage doit être un nombre et ne peut pas être vide. not_valid: Le montant de la remise en pourcentage doit être configuré. + catalog_promotion_scope: + type: + not_blank: Veuillez choisir un type de périmètre. + invalid: Le type de périmètre de promotion catalogue n'est pas valide. Les types disponibles sont {{ available_scope_types }}. promotion: code: unique: Ce code de promotion existe déjà. @@ -37,8 +44,11 @@ sylius: min_length: Le nom de la promotion doit contenir au moins 1 caractère.|Le nom de la promotion doit contenir au moins {{ limit }} caractères. not_blank: Veuillez saisir le nom de la promotion. promotion_action: + invalid_type: Le type d'action de promotion n'est pas valide. Les types d'action disponibles sont {{ available_action_types }}. percentage_discount_configuration: not_in_range: Le pourcentage de remise doit être compris entre 0% et 100%. + promotion_rule: + invalid_type: Le type de règle de promotion n'est pas valide. Les types de règles disponibles sont {{ available_rule_types }}. promotion_coupon: code: max_length: Le code promo ne doit pas contenir plus d'un caractère.|Le code promo ne doit pas dépasser les {{ limit }} caractères. @@ -47,6 +57,9 @@ sylius: regex: Le code de promotion peut uniquement être constitué de lettres, chiffres, tirets et traits de soulignement. unique: Cette promotion existe déjà. is_invalid: Ce code promo n'est pas valide. + promotion: + not_blank: Veuillez fournir une promotion pour ce coupon. + not_coupon_based: Seules les promotions basées sur les coupons peuvent avoir des coupons. usage_limit: min: La limite d'utilisation de coupon doit être au moins de {{ limit }}. promotion_coupon_generator_instruction: @@ -61,3 +74,8 @@ sylius: possible_generation_amount: Longueur de code ou nombre de bons de réduction invalide. Il n'est pas possible de générer %expectedAmount% coupons uniques avec la longueur de code %codeLength%. Le nombre possible à générer est de %possibleAmount%. usage_limit: min: La limite d'utilisation du code promo généré doit être au moins de {{ limit }}. + translation: + locale: + not_blank: Vous devez entrer une valeur. + invalid: Ce paramètre régional n'est pas valide. + unique: Une traduction pour la locale {{ value }} existe déjà. diff --git a/src/Sylius/Bundle/PromotionBundle/Tests/Command/GenerateCouponsCommandTest.php b/src/Sylius/Bundle/PromotionBundle/Tests/Console/Command/GenerateCouponsCommandTest.php similarity index 92% rename from src/Sylius/Bundle/PromotionBundle/Tests/Command/GenerateCouponsCommandTest.php rename to src/Sylius/Bundle/PromotionBundle/Tests/Console/Command/GenerateCouponsCommandTest.php index 0b143d9ccc..fe2917ff36 100644 --- a/src/Sylius/Bundle/PromotionBundle/Tests/Command/GenerateCouponsCommandTest.php +++ b/src/Sylius/Bundle/PromotionBundle/Tests/Console/Command/GenerateCouponsCommandTest.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Sylius\Bundle\PromotionBundle\Tests\Command; +namespace Sylius\Bundle\PromotionBundle\Tests\Console\Command; use InvalidArgumentException; use PHPUnit\Framework\MockObject\MockObject; @@ -24,7 +24,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; -class GenerateCouponsCommandTest extends KernelTestCase +final class GenerateCouponsCommandTest extends KernelTestCase { private Command $command; @@ -72,7 +72,7 @@ class GenerateCouponsCommandTest extends KernelTestCase // the output of the command in the console $output = $this->commandTester->getDisplay(); $this->assertNotEquals($this->commandTester->getStatusCode(), 0); - $this->assertContains('No promotion found with this code', $output); + $this->assertStringContainsString('No promotion found with this code', $output); } /** @@ -97,7 +97,7 @@ class GenerateCouponsCommandTest extends KernelTestCase // the output of the command in the console $output = $this->commandTester->getDisplay(); $this->assertNotEquals($this->commandTester->getStatusCode(), 0); - $this->assertContains('This promotion is not coupon based', $output); + $this->assertStringContainsString('This promotion is not coupon based', $output); } /** @@ -136,7 +136,7 @@ class GenerateCouponsCommandTest extends KernelTestCase // the output of the command in the console $output = $this->commandTester->getDisplay(); $this->assertEquals($this->commandTester->getStatusCode(), 1); - $this->assertContains('Could not generate', $output); + $this->assertStringContainsString('Could not generate', $output); } /** @@ -175,7 +175,7 @@ class GenerateCouponsCommandTest extends KernelTestCase // the output of the command in the console $output = $this->commandTester->getDisplay(); $this->assertEquals($this->commandTester->getStatusCode(), 0); - $this->assertContains('Coupons have been generated', $output); + $this->assertStringContainsString('Coupons have been generated', $output); } /** @@ -217,6 +217,6 @@ class GenerateCouponsCommandTest extends KernelTestCase // the output of the command in the console $output = $this->commandTester->getDisplay(); $this->assertEquals($this->commandTester->getStatusCode(), 0); - $this->assertContains('Coupons have been generated', $output); + $this->assertStringContainsString('Coupons have been generated', $output); } } diff --git a/src/Sylius/Bundle/PromotionBundle/Tests/DependencyInjection/SyliusPromotionExtensionTest.php b/src/Sylius/Bundle/PromotionBundle/Tests/DependencyInjection/SyliusPromotionExtensionTest.php new file mode 100644 index 0000000000..6b48e2fe63 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Tests/DependencyInjection/SyliusPromotionExtensionTest.php @@ -0,0 +1,254 @@ +container->setDefinition( + 'acme.promotion_action_autoconfigured', + (new Definition()) + ->setClass(PromotionActionStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.promotion_action_autoconfigured', + AsPromotionAction::SERVICE_TAG, + [ + 'type' => 'test', + 'label' => 'Test', + 'form-type' => 'SomeFormType', + 'priority' => 10, + ], + ); + } + + /** @test */ + public function it_autoconfigures_promotion_coupon_eligibility_checker_with_attribute(): void + { + $this->container->setDefinition( + 'acme.promotion_coupon_eligibility_checker_autoconfigured', + (new Definition()) + ->setClass(PromotionCouponEligibilityCheckerStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.promotion_coupon_eligibility_checker_autoconfigured', + AsPromotionCouponEligibilityChecker::SERVICE_TAG, + ['priority' => 20], + ); + } + + /** @test */ + public function it_autoconfigures_promotion_eligibility_checker_with_attribute(): void + { + $this->container->setDefinition( + 'acme.promotion_eligibility_checker_autoconfigured', + (new Definition()) + ->setClass(PromotionEligibilityCheckerStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.promotion_eligibility_checker_autoconfigured', + AsPromotionEligibilityChecker::SERVICE_TAG, + ['priority' => 30], + ); + } + + /** @test */ + public function it_autoconfigures_promotion_rule_checker_with_attribute(): void + { + $this->container->setDefinition( + 'acme.promotion_rule_checker_autoconfigured', + (new Definition()) + ->setClass(PromotionRuleCheckerStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.promotion_rule_checker_autoconfigured', + AsPromotionRuleChecker::SERVICE_TAG, + [ + 'type' => 'test', + 'label' => 'Test', + 'form-type' => 'SomeFormType', + 'priority' => 40, + ], + ); + } + + /** @test */ + public function it_loads_promotion_action_validation_groups_parameter_value_properly(): void + { + $this->load([ + 'promotion_action' => [ + 'validation_groups' => [ + 'order_percentage_discount' => ['sylius', 'order_percentage_discount'], + 'order_fixed_discount' => ['sylius', 'order_fixed_discount'], + ], + ], + ]); + + $this->assertContainerBuilderHasParameter( + 'sylius.promotion.promotion_action.validation_groups', + ['order_percentage_discount' => ['sylius', 'order_percentage_discount'], 'order_fixed_discount' => ['sylius', 'order_fixed_discount']], + ); + } + + /** @test */ + public function it_loads_empty_promotion_action_validation_groups_parameter_value(): void + { + $this->load(); + + $this->assertContainerBuilderHasParameter( + 'sylius.promotion.promotion_action.validation_groups', + [], + ); + } + + /** @test */ + public function it_loads_promotion_rule_validation_groups_parameter_value_properly(): void + { + $this->load([ + 'promotion_rule' => [ + 'validation_groups' => [ + 'cart_quantity' => ['sylius', 'cart_quantity'], + 'nth_order' => ['sylius', 'nth_order'], + ], + ], + ]); + + $this->assertContainerBuilderHasParameter( + 'sylius.promotion.promotion_rule.validation_groups', + ['cart_quantity' => ['sylius', 'cart_quantity'], 'nth_order' => ['sylius', 'nth_order']], + ); + } + + /** @test */ + public function it_loads_empty_promotion_rule_validation_groups_parameter_value(): void + { + $this->load(); + + $this->assertContainerBuilderHasParameter( + 'sylius.promotion.promotion_rule.validation_groups', + [], + ); + } + + /** @test */ + public function it_loads_catalog_promotion_action_validation_groups_parameter_value_properly(): void + { + $this->load([ + 'catalog_promotion_action' => [ + 'validation_groups' => [ + 'something' => ['sylius', 'something'], + 'test' => ['sylius', 'test'], + ], + ], + ]); + + $this->assertContainerBuilderHasParameter( + 'sylius.promotion.catalog_promotion_action.validation_groups', + ['something' => ['sylius', 'something'], 'test' => ['sylius', 'test']], + ); + } + + /** @test */ + public function it_loads_empty_catalog_promotion_action_validation_groups_parameter_value(): void + { + $this->load(); + + $this->assertContainerBuilderHasParameter( + 'sylius.promotion.catalog_promotion_action.validation_groups', + [], + ); + } + + /** @test */ + public function it_loads_catalog_promotion_scope_validation_groups_parameter_value_properly(): void + { + $this->load([ + 'catalog_promotion_scope' => [ + 'validation_groups' => [ + 'something' => ['sylius', 'something'], + 'test' => ['sylius', 'test'], + ], + ], + ]); + + $this->assertContainerBuilderHasParameter( + 'sylius.promotion.catalog_promotion_scope.validation_groups', + ['something' => ['sylius', 'something'], 'test' => ['sylius', 'test']], + ); + } + + /** @test */ + public function it_loads_empty_catalog_promotion_scope_validation_groups_parameter_value(): void + { + $this->load(); + + $this->assertContainerBuilderHasParameter( + 'sylius.promotion.catalog_promotion_scope.validation_groups', + [], + ); + } + + protected function getContainerExtensions(): array + { + return [new SyliusPromotionExtension()]; + } + + protected function getMinimalConfiguration(): array + { + return [ + 'resources' => [ + 'promotion_subject' => [ + 'classes' => [ + 'model' => PromotionSubjectInterface::class, + ], + ], + ], + ]; + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Tests/Stub/PromotionActionStub.php b/src/Sylius/Bundle/PromotionBundle/Tests/Stub/PromotionActionStub.php new file mode 100644 index 0000000000..258383bff0 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Tests/Stub/PromotionActionStub.php @@ -0,0 +1,32 @@ +> $validationGroups */ + public function __construct(private array $validationGroups) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof CatalogPromotionActionGroup) { + throw new UnexpectedTypeException($constraint, CatalogPromotionActionGroup::class); + } + + if (!$value instanceof CatalogPromotionActionInterface) { + throw new UnexpectedTypeException($value, CatalogPromotionActionInterface::class); + } + + $type = $value->getType(); + if (null === $type || '' === $type) { + return; + } + + $validator = $this->context->getValidator()->inContext($this->context); + + $groups = $this->validationGroups[$type] ?? null; + if (null !== $groups) { + $validator->validate(value: $value, groups: $groups); + + if ($this->context->getViolations()->count() > 0) { + return; + } + } + + $validator->validate( + $value, + new CatalogPromotionAction(groups: $constraint->groups), + $groups ?? $constraint->groups, + ); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionActionTypeValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionActionTypeValidator.php new file mode 100644 index 0000000000..94464eb5ff --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionActionTypeValidator.php @@ -0,0 +1,53 @@ + $actionTypes */ + public function __construct(private array $actionTypes) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof CatalogPromotionActionType) { + throw new UnexpectedTypeException($constraint, CatalogPromotionScopeType::class); + } + + if (!$value instanceof CatalogPromotionActionInterface) { + throw new UnexpectedTypeException($value, CatalogPromotionActionInterface::class); + } + + $type = $value->getType(); + if (null === $type || '' === $type) { + return; + } + + if (!in_array($type, $this->actionTypes, true)) { + $this->context->buildViolation($constraint->invalidType) + ->setParameter('{{ available_action_types }}', implode(', ', $this->actionTypes)) + ->atPath('type') + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionActionValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionActionValidator.php index 5ee289cc1d..3251459f98 100644 --- a/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionActionValidator.php +++ b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionActionValidator.php @@ -15,11 +15,16 @@ namespace Sylius\Bundle\PromotionBundle\Validator; use Sylius\Bundle\PromotionBundle\Validator\CatalogPromotionAction\ActionValidatorInterface; use Sylius\Bundle\PromotionBundle\Validator\Constraints\CatalogPromotionAction; -use Sylius\Component\Promotion\Model\CatalogPromotionActionInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; +trigger_deprecation( + 'sylius/promotion-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0, use the usual symfony logic for validation.', + CatalogPromotionActionValidator::class, +); final class CatalogPromotionActionValidator extends ConstraintValidator { private array $actionValidators; @@ -29,18 +34,11 @@ final class CatalogPromotionActionValidator extends ConstraintValidator $this->actionValidators = $actionValidators instanceof \Traversable ? iterator_to_array($actionValidators) : $actionValidators; } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var CatalogPromotionAction $constraint */ Assert::isInstanceOf($constraint, CatalogPromotionAction::class); - /** @var CatalogPromotionActionInterface $value */ - if (!in_array($value->getType(), $this->actionTypes, true)) { - $this->context->buildViolation($constraint->invalidType)->atPath('type')->addViolation(); - - return; - } - $type = $value->getType(); if (!array_key_exists($type, $this->actionValidators)) { return; diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScope/ScopeValidatorInterface.php b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScope/ScopeValidatorInterface.php index 157a66b514..2b8eb35314 100644 --- a/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScope/ScopeValidatorInterface.php +++ b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScope/ScopeValidatorInterface.php @@ -16,6 +16,12 @@ namespace Sylius\Bundle\PromotionBundle\Validator\CatalogPromotionScope; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Context\ExecutionContextInterface; +trigger_deprecation( + 'sylius/promotion-bundle', + '1.13', + 'The "%s" interface is deprecated and will be removed in Sylius 2.0, use the usual symfony logic for validation.', + ScopeValidatorInterface::class, +); interface ScopeValidatorInterface { public function validate(array $configuration, Constraint $constraint, ExecutionContextInterface $context): void; diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeGroupValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeGroupValidator.php new file mode 100644 index 0000000000..ca1f61fd68 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeGroupValidator.php @@ -0,0 +1,62 @@ +> $validationGroups */ + public function __construct(private array $validationGroups) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof CatalogPromotionScopeGroup) { + throw new UnexpectedTypeException($constraint, CatalogPromotionScopeGroup::class); + } + + if (!$value instanceof CatalogPromotionScopeInterface) { + throw new UnexpectedTypeException($value, CatalogPromotionScopeInterface::class); + } + + $type = $value->getType(); + if (null === $type || '' === $type) { + return; + } + + $validator = $this->context->getValidator()->inContext($this->context); + + $groups = $this->validationGroups[$type] ?? null; + if (null !== $groups) { + $validator->validate(value: $value, groups: $groups); + + if ($this->context->getViolations()->count() > 0) { + return; + } + } + + $validator->validate( + $value, + new CatalogPromotionScope(groups: $constraint->groups), + $groups ?? $constraint->groups, + ); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeTypeValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeTypeValidator.php new file mode 100644 index 0000000000..606edac500 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeTypeValidator.php @@ -0,0 +1,52 @@ + $scopeTypes */ + public function __construct(private array $scopeTypes) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof CatalogPromotionScopeType) { + throw new UnexpectedTypeException($constraint, CatalogPromotionScopeType::class); + } + + if (!$value instanceof CatalogPromotionScopeInterface) { + throw new UnexpectedTypeException($value, CatalogPromotionScopeInterface::class); + } + + $type = $value->getType(); + if (null === $type || '' === $type) { + return; + } + + if (!in_array($type, $this->scopeTypes, true)) { + $this->context->buildViolation($constraint->invalidType) + ->setParameter('{{ available_scope_types }}', implode(', ', $this->scopeTypes)) + ->atPath('type') + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeValidator.php index b083934848..bb2ffc3668 100644 --- a/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeValidator.php +++ b/src/Sylius/Bundle/PromotionBundle/Validator/CatalogPromotionScopeValidator.php @@ -15,11 +15,16 @@ namespace Sylius\Bundle\PromotionBundle\Validator; use Sylius\Bundle\PromotionBundle\Validator\CatalogPromotionScope\ScopeValidatorInterface; use Sylius\Bundle\PromotionBundle\Validator\Constraints\CatalogPromotionScope; -use Sylius\Component\Promotion\Model\CatalogPromotionScopeInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Webmozart\Assert\Assert; +trigger_deprecation( + 'sylius/promotion-bundle', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0, use the usual symfony logic for validation.', + CatalogPromotionScopeValidator::class, +); final class CatalogPromotionScopeValidator extends ConstraintValidator { private array $scopeValidators; @@ -29,18 +34,11 @@ final class CatalogPromotionScopeValidator extends ConstraintValidator $this->scopeValidators = $scopeValidators instanceof \Traversable ? iterator_to_array($scopeValidators) : $scopeValidators; } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var CatalogPromotionScope $constraint */ Assert::isInstanceOf($constraint, CatalogPromotionScope::class); - /** @var CatalogPromotionScopeInterface $value */ - if (!in_array($value->getType(), $this->scopeTypes, true)) { - $this->context->buildViolation($constraint->invalidType)->atPath('type')->addViolation(); - - return; - } - $type = $value->getType(); if (!array_key_exists($type, $this->scopeValidators)) { return; diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/Constraints/CatalogPromotionActionGroup.php b/src/Sylius/Bundle/PromotionBundle/Validator/Constraints/CatalogPromotionActionGroup.php new file mode 100644 index 0000000000..fb138387a7 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Validator/Constraints/CatalogPromotionActionGroup.php @@ -0,0 +1,29 @@ +getCodeLength() || null === $value->getAmount()) { return; } - /** @var PromotionCouponGeneratorInstructionInterface $value */ - Assert::isInstanceOf($value, PromotionCouponGeneratorInstructionInterface::class); + /** @var ReadablePromotionCouponGeneratorInstructionInterface $value */ + Assert::isInstanceOf($value, ReadablePromotionCouponGeneratorInstructionInterface::class); /** @var CouponPossibleGenerationAmount $constraint */ Assert::isInstanceOf($constraint, CouponPossibleGenerationAmount::class); diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionActionGroupValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionActionGroupValidator.php new file mode 100644 index 0000000000..d0a9f0c1b8 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionActionGroupValidator.php @@ -0,0 +1,45 @@ +> $validationGroups */ + public function __construct(private array $validationGroups) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof PromotionActionGroup) { + throw new UnexpectedTypeException($constraint, PromotionActionGroup::class); + } + + if (!$value instanceof PromotionActionInterface) { + throw new UnexpectedValueException($value, PromotionActionInterface::class); + } + + /** @var string[] $groups */ + $groups = $this->validationGroups[$value->getType()] ?? $constraint->groups; + $validator = $this->context->getValidator()->inContext($this->context); + $validator->validate(value: $value, groups: $groups); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionActionTypeValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionActionTypeValidator.php new file mode 100644 index 0000000000..4a657a995d --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionActionTypeValidator.php @@ -0,0 +1,47 @@ + $actionTypes */ + public function __construct(private array $actionTypes) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof PromotionActionType) { + throw new UnexpectedTypeException($constraint, PromotionActionType::class); + } + + if (!$value instanceof PromotionActionInterface) { + throw new UnexpectedValueException($value, PromotionActionInterface::class); + } + + if (!array_key_exists($value->getType(), $this->actionTypes)) { + $this->context->buildViolation($constraint->invalidType) + ->setParameter('{{ available_action_types }}', implode(', ', array_keys($this->actionTypes))) + ->atPath('type') + ->addViolation(); + } + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionDateRangeValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionDateRangeValidator.php index ea2076d706..7755b9a1b3 100644 --- a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionDateRangeValidator.php +++ b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionDateRangeValidator.php @@ -21,7 +21,7 @@ use Webmozart\Assert\Assert; final class PromotionDateRangeValidator extends ConstraintValidator { - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { if (null === $value) { return; diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionNotCouponBasedValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionNotCouponBasedValidator.php new file mode 100644 index 0000000000..3020c4ecf0 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionNotCouponBasedValidator.php @@ -0,0 +1,52 @@ +getPromotion(); + if (null === $promotion) { + return; + } + + if (!$promotion->isCouponBased()) { + $this->context + ->buildViolation($constraint->message) + ->atPath('promotion') + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionRuleGroupValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionRuleGroupValidator.php new file mode 100644 index 0000000000..fb1c1f7922 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionRuleGroupValidator.php @@ -0,0 +1,45 @@ +> $validationGroups */ + public function __construct(private array $validationGroups) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof PromotionRuleGroup) { + throw new UnexpectedTypeException($constraint, PromotionRuleGroup::class); + } + + if (!$value instanceof PromotionRuleInterface) { + throw new UnexpectedValueException($value, PromotionRuleInterface::class); + } + + /** @var string[] $groups */ + $groups = $this->validationGroups[$value->getType()] ?? $constraint->groups; + $validator = $this->context->getValidator()->inContext($this->context); + $validator->validate(value: $value, groups: $groups); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionRuleTypeValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionRuleTypeValidator.php new file mode 100644 index 0000000000..b6a85bd130 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionRuleTypeValidator.php @@ -0,0 +1,47 @@ + $ruleTypes */ + public function __construct(private array $ruleTypes) + { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$constraint instanceof PromotionRuleType) { + throw new UnexpectedTypeException($constraint, PromotionRuleType::class); + } + + if (!$value instanceof PromotionRuleInterface) { + throw new UnexpectedValueException($value, PromotionRuleInterface::class); + } + + if (!array_key_exists($value->getType(), $this->ruleTypes)) { + $this->context->buildViolation($constraint->invalidType) + ->setParameter('{{ available_rule_types }}', implode(', ', array_keys($this->ruleTypes))) + ->atPath('type') + ->addViolation(); + } + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionSubjectCouponValidator.php b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionSubjectCouponValidator.php index 46abbb9a59..2335c9d042 100644 --- a/src/Sylius/Bundle/PromotionBundle/Validator/PromotionSubjectCouponValidator.php +++ b/src/Sylius/Bundle/PromotionBundle/Validator/PromotionSubjectCouponValidator.php @@ -26,7 +26,7 @@ final class PromotionSubjectCouponValidator extends ConstraintValidator { } - public function validate($value, Constraint $constraint): void + public function validate(mixed $value, Constraint $constraint): void { /** @var PromotionSubjectCoupon $constraint */ Assert::isInstanceOf($constraint, PromotionSubjectCoupon::class); diff --git a/src/Sylius/Bundle/PromotionBundle/composer.json b/src/Sylius/Bundle/PromotionBundle/composer.json index 5595c292b0..bbffbc1d28 100644 --- a/src/Sylius/Bundle/PromotionBundle/composer.json +++ b/src/Sylius/Bundle/PromotionBundle/composer.json @@ -27,14 +27,14 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "stof/doctrine-extensions-bundle": "^1.4", - "sylius/calendar": "^0.3 || ^0.4", - "sylius/money-bundle": "^1.12", - "sylius/promotion": "^1.12", + "sylius/calendar": "^0.3 || ^0.4 || ^0.5", + "sylius/money-bundle": "^1.13", + "sylius/promotion": "^1.13", "sylius/registry": "^1.5", "sylius/resource-bundle": "^1.9", - "symfony/framework-bundle": "^5.4 || ^6.0" + "symfony/framework-bundle": "^5.4.21 || ^6.4" }, "conflict": { "doctrine/orm": "2.15.2", @@ -45,12 +45,13 @@ "doctrine/orm": "^2.13", "matthiasnoback/symfony-dependency-injection-test": "^4.2", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0", - "symfony/validator": "^5.4 || ^6.0" + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4", + "symfony/validator": "^5.4.21 || ^6.4" }, "config": { "allow-plugins": { @@ -59,7 +60,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { @@ -70,6 +71,7 @@ "autoload-dev": { "psr-4": { "Sylius\\Bundle\\PromotionBundle\\spec\\": "spec/", + "Sylius\\Bundle\\PromotionBundle\\Tests\\": "Tests/", "AppBundle\\": "test/src/AppBundle/" }, "classmap": [ diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionActionGroupValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionActionGroupValidatorSpec.php new file mode 100644 index 0000000000..9fd56e0322 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionActionGroupValidatorSpec.php @@ -0,0 +1,179 @@ + [ + 'test_group', + ], + 'another_test' => [ + 'another_test_group', + ], + ]; + + function let(ExecutionContextInterface $context): void + { + $this->beConstructedWith(self::VALIDATION_GROUPS); + $this->initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldHaveType(ConstraintValidator::class); + } + + function it_throws_exception_when_constraint_is_not_catalog_promotion_action_group( + CatalogPromotionActionInterface $action, + Constraint $constraint, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$action, $constraint]) + ; + } + + function it_throws_exception_when_value_is_not_catalog_promotion_action(): void + { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [new \stdClass(), new CatalogPromotionActionGroup()]) + ; + } + + function it_does_nothing_when_type_is_null( + ExecutionContextInterface $context, + CatalogPromotionActionInterface $action, + ): void { + $action->getType()->willReturn(null); + + $context->getValidator()->shouldNotBeCalled(); + + $this->validate($action, new CatalogPromotionActionGroup()); + } + + function it_does_nothing_when_type_is_an_empty_string( + ExecutionContextInterface $context, + CatalogPromotionActionInterface $action, + ): void { + $action->getType()->willReturn(''); + + $context->getValidator()->shouldNotBeCalled(); + + $this->validate($action, new CatalogPromotionActionGroup()); + } + + function it_passes_configured_validation_groups_for_further_validation( + ExecutionContextInterface $context, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ConstraintViolationListInterface $violationList, + CatalogPromotionActionInterface $action, + ): void { + $constraint = new CatalogPromotionActionGroup(); + + $action->getType()->willReturn('test'); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator + ->validate($action, null, ['test_group']) + ->shouldBeCalled() + ->willReturn($contextualValidator) + ; + + $context->getViolations()->willReturn($violationList); + $violationList->count()->willReturn(1); + + $contextualValidator + ->validate($action, new CatalogPromotionAction(null, $constraint->groups), ['test_group']) + ->shouldNotBeCalled() + ; + + $this->validate($action, $constraint); + } + + function it_falls_back_to_previous_abstraction_when_no_violation_has_been_added( + ExecutionContextInterface $context, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ConstraintViolationListInterface $violationList, + CatalogPromotionActionInterface $action, + ): void { + $constraint = new CatalogPromotionActionGroup(); + + $action->getType()->willReturn('test'); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator + ->validate($action, null, ['test_group']) + ->willReturn($contextualValidator) + ->shouldBeCalled() + ; + + $context->getViolations()->willReturn($violationList); + $violationList->count()->willReturn(0); + + $contextualValidator + ->validate($action, new CatalogPromotionAction(null, $constraint->groups), ['test_group']) + ->shouldBeCalled() + ->willReturn($contextualValidator) + ; + + $this->validate($action, $constraint); + } + + function it_falls_back_to_previous_abstraction_when_no_validation_group_for_the_action_type_has_been_passed( + ExecutionContextInterface $context, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + CatalogPromotionActionInterface $action, + ): void { + $constraint = new CatalogPromotionActionGroup(); + + $action->getType()->willReturn('not_existing_type'); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator->validate($action, null, Argument::any())->shouldNotBeCalled(); + + $context->getViolations()->shouldNotBeCalled(); + + $contextualValidator + ->validate($action, new CatalogPromotionAction(null, $constraint->groups), $constraint->groups) + ->shouldBeCalled() + ->willReturn($contextualValidator) + ; + + $this->validate($action, $constraint); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionActionTypeValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionActionTypeValidatorSpec.php new file mode 100644 index 0000000000..eae17f7912 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionActionTypeValidatorSpec.php @@ -0,0 +1,113 @@ +beConstructedWith(self::ACTION_TYPES); + $this->initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldHaveType(ConstraintValidator::class); + } + + function it_throws_exception_when_constraint_is_not_catalog_promotion_action_type( + Constraint $constraint, + CatalogPromotionActionInterface $action, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$action, $constraint]) + ; + } + + function it_throws_exception_when_value_is_not_catalog_promotion_action(): void + { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [new \stdClass(), new CatalogPromotionActionType()]) + ; + } + + function it_does_nothing_when_passed_action_has_null_as_type( + ExecutionContextInterface $context, + CatalogPromotionActionInterface $action, + ): void { + $action->getType()->willReturn(null); + + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate($action, new CatalogPromotionActionType()); + } + + function it_does_nothing_when_passed_action_has_an_empty_string_as_type( + ExecutionContextInterface $context, + CatalogPromotionActionInterface $action, + ): void { + $action->getType()->willReturn(''); + + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate($action, new CatalogPromotionActionType()); + } + + function it_does_nothing_when_catalog_promotion_action_has_valid_type( + ExecutionContextInterface $context, + CatalogPromotionActionInterface $action, + ): void { + $action->getType()->willReturn('test'); + + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate($action, new CatalogPromotionActionType()); + } + + function it_adds_violation_when_type_is_unknown( + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $violationBuilder, + CatalogPromotionActionInterface $action, + ): void { + $constraint = new CatalogPromotionActionType(); + $action->getType()->willReturn('not_existing_type'); + + $context->buildViolation($constraint->invalidType)->willReturn($violationBuilder); + $violationBuilder + ->setParameter('{{ available_action_types }}', implode(', ', self::ACTION_TYPES)) + ->willReturn($violationBuilder) + ; + $violationBuilder->atPath('type')->willReturn($violationBuilder); + $violationBuilder->addViolation()->shouldBeCalled(); + + $this->validate($action, $constraint); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionActionValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionActionValidatorSpec.php index d43c72ba37..0afac2a8c6 100644 --- a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionActionValidatorSpec.php +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionActionValidatorSpec.php @@ -14,12 +14,12 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\PromotionBundle\Validator; use PhpSpec\ObjectBehavior; +use Prophecy\Argument; use Sylius\Bundle\PromotionBundle\Validator\CatalogPromotionAction\ActionValidatorInterface; use Sylius\Bundle\PromotionBundle\Validator\Constraints\CatalogPromotionAction; use Sylius\Component\Promotion\Model\CatalogPromotionActionInterface; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; final class CatalogPromotionActionValidatorSpec extends ObjectBehavior { @@ -44,16 +44,13 @@ final class CatalogPromotionActionValidatorSpec extends ObjectBehavior $this->shouldHaveType(ConstraintValidator::class); } - function it_adds_violation_if_catalog_promotion_action_has_invalid_type( + function it_does_nothing_when_passed_action_type_has_no_validators( ExecutionContextInterface $executionContext, - ConstraintViolationBuilderInterface $constraintViolationBuilder, CatalogPromotionActionInterface $action, ): void { - $action->getType()->willReturn('wrong_type'); + $action->getType()->willReturn('custom'); - $executionContext->buildViolation('sylius.catalog_promotion_action.invalid_type')->willReturn($constraintViolationBuilder); - $constraintViolationBuilder->atPath('type')->willReturn($constraintViolationBuilder); - $constraintViolationBuilder->addViolation()->shouldBeCalled(); + $executionContext->buildViolation(Argument::any())->shouldNotBeCalled(); $this->validate($action, new CatalogPromotionAction()); } diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionScopeGroupValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionScopeGroupValidatorSpec.php new file mode 100644 index 0000000000..317acf4618 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionScopeGroupValidatorSpec.php @@ -0,0 +1,179 @@ + [ + 'test_group', + ], + 'another_test' => [ + 'another_test_group', + ], + ]; + + function let(ExecutionContextInterface $context): void + { + $this->beConstructedWith(self::VALIDATION_GROUPS); + $this->initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldHaveType(ConstraintValidator::class); + } + + function it_throws_exception_when_constraint_is_not_catalog_promotion_scope_group( + CatalogPromotionScopeInterface $scope, + Constraint $constraint, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$scope, $constraint]) + ; + } + + function it_throws_exception_when_value_is_not_catalog_promotion_scope(): void + { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [new \stdClass(), new CatalogPromotionScopeGroup()]) + ; + } + + function it_does_nothing_when_type_is_null( + ExecutionContextInterface $context, + CatalogPromotionScopeInterface $scope, + ): void { + $scope->getType()->willReturn(null); + + $context->getValidator()->shouldNotBeCalled(); + + $this->validate($scope, new CatalogPromotionScopeGroup()); + } + + function it_does_nothing_when_type_is_an_empty_string( + ExecutionContextInterface $context, + CatalogPromotionScopeInterface $scope, + ): void { + $scope->getType()->willReturn(''); + + $context->getValidator()->shouldNotBeCalled(); + + $this->validate($scope, new CatalogPromotionScopeGroup()); + } + + function it_passes_configured_validation_groups_for_further_validation( + ExecutionContextInterface $context, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ConstraintViolationListInterface $violationList, + CatalogPromotionScopeInterface $scope, + ): void { + $constraint = new CatalogPromotionScopeGroup(); + + $scope->getType()->willReturn('test'); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator + ->validate($scope, null, ['test_group']) + ->shouldBeCalled() + ->willReturn($contextualValidator) + ; + + $context->getViolations()->willReturn($violationList); + $violationList->count()->willReturn(1); + + $contextualValidator + ->validate($scope, new CatalogPromotionScope(null, $constraint->groups), ['test_group']) + ->shouldNotBeCalled() + ; + + $this->validate($scope, $constraint); + } + + function it_falls_back_to_previous_abstraction_when_no_violation_has_been_added( + ExecutionContextInterface $context, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ConstraintViolationListInterface $violationList, + CatalogPromotionScopeInterface $scope, + ): void { + $constraint = new CatalogPromotionScopeGroup(); + + $scope->getType()->willReturn('test'); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator + ->validate($scope, null, ['test_group']) + ->willReturn($contextualValidator) + ->shouldBeCalled() + ; + + $context->getViolations()->willReturn($violationList); + $violationList->count()->willReturn(0); + + $contextualValidator + ->validate($scope, new CatalogPromotionScope(null, $constraint->groups), ['test_group']) + ->shouldBeCalled() + ->willReturn($contextualValidator) + ; + + $this->validate($scope, $constraint); + } + + function it_falls_back_to_previous_abstraction_when_no_validation_group_for_the_scope_type_has_been_passed( + ExecutionContextInterface $context, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + CatalogPromotionScopeInterface $scope, + ): void { + $constraint = new CatalogPromotionScopeGroup(); + + $scope->getType()->willReturn('not_existing_type'); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator->validate($scope, null, Argument::any())->shouldNotBeCalled(); + + $context->getViolations()->shouldNotBeCalled(); + + $contextualValidator + ->validate($scope, new CatalogPromotionScope(null, $constraint->groups), $constraint->groups) + ->shouldBeCalled() + ->willReturn($contextualValidator) + ; + + $this->validate($scope, $constraint); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionScopeTypeValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionScopeTypeValidatorSpec.php new file mode 100644 index 0000000000..d477451461 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionScopeTypeValidatorSpec.php @@ -0,0 +1,113 @@ +beConstructedWith(self::SCOPE_TYPES); + $this->initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldHaveType(ConstraintValidator::class); + } + + function it_throws_exception_when_constraint_is_not_catalog_promotion_scope_type( + Constraint $constraint, + CatalogPromotionScopeInterface $scope, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$scope, $constraint]) + ; + } + + function it_throws_exception_when_value_is_not_catalog_promotion_scope(): void + { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [new \stdClass(), new CatalogPromotionScopeType()]) + ; + } + + function it_does_nothing_when_passed_scope_has_null_as_type( + ExecutionContextInterface $context, + CatalogPromotionScopeInterface $scope, + ): void { + $scope->getType()->willReturn(null); + + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate($scope, new CatalogPromotionScopeType()); + } + + function it_does_nothing_when_passed_scope_has_an_empty_string_as_type( + ExecutionContextInterface $context, + CatalogPromotionScopeInterface $scope, + ): void { + $scope->getType()->willReturn(''); + + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate($scope, new CatalogPromotionScopeType()); + } + + function it_does_nothing_when_catalog_promotion_scope_has_valid_type( + ExecutionContextInterface $context, + CatalogPromotionScopeInterface $scope, + ): void { + $scope->getType()->willReturn('test'); + + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate($scope, new CatalogPromotionScopeType()); + } + + function it_adds_violation_when_type_is_unknown( + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $violationBuilder, + CatalogPromotionScopeInterface $scope, + ): void { + $constraint = new CatalogPromotionScopeType(); + $scope->getType()->willReturn('not_existing_type'); + + $context->buildViolation($constraint->invalidType)->willReturn($violationBuilder); + $violationBuilder + ->setParameter('{{ available_scope_types }}', implode(', ', self::SCOPE_TYPES)) + ->willReturn($violationBuilder) + ; + $violationBuilder->atPath('type')->willReturn($violationBuilder); + $violationBuilder->addViolation()->shouldBeCalled(); + + $this->validate($scope, $constraint); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionScopeValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionScopeValidatorSpec.php index 542f1fa3c9..74399d4017 100644 --- a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionScopeValidatorSpec.php +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CatalogPromotionScopeValidatorSpec.php @@ -14,12 +14,12 @@ declare(strict_types=1); namespace spec\Sylius\Bundle\PromotionBundle\Validator; use PhpSpec\ObjectBehavior; +use Prophecy\Argument; use Sylius\Bundle\PromotionBundle\Validator\CatalogPromotionScope\ScopeValidatorInterface; use Sylius\Bundle\PromotionBundle\Validator\Constraints\CatalogPromotionScope; use Sylius\Component\Promotion\Model\CatalogPromotionScopeInterface; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Context\ExecutionContextInterface; -use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; final class CatalogPromotionScopeValidatorSpec extends ObjectBehavior { @@ -47,16 +47,13 @@ final class CatalogPromotionScopeValidatorSpec extends ObjectBehavior $this->shouldHaveType(ConstraintValidator::class); } - function it_adds_violation_if_catalog_promotion_scope_has_invalid_type( + function it_does_nothing_when_passed_scope_type_has_no_validators( ExecutionContextInterface $executionContext, - ConstraintViolationBuilderInterface $constraintViolationBuilder, CatalogPromotionScopeInterface $scope, ): void { - $scope->getType()->willReturn('wrong_type'); + $scope->getType()->willReturn('custom'); - $executionContext->buildViolation('sylius.catalog_promotion_scope.invalid_type')->willReturn($constraintViolationBuilder); - $constraintViolationBuilder->atPath('type')->willReturn($constraintViolationBuilder); - $constraintViolationBuilder->addViolation()->shouldBeCalled(); + $executionContext->buildViolation(Argument::any())->shouldNotBeCalled(); $this->validate($scope, new CatalogPromotionScope()); } diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CouponGenerationAmountValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CouponGenerationAmountValidatorSpec.php index 25a783c984..abb275c019 100644 --- a/src/Sylius/Bundle/PromotionBundle/spec/Validator/CouponGenerationAmountValidatorSpec.php +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/CouponGenerationAmountValidatorSpec.php @@ -18,7 +18,7 @@ use Prophecy\Argument; use Sylius\Bundle\PromotionBundle\Validator\Constraints\CouponPossibleGenerationAmount; use Sylius\Bundle\PromotionBundle\Validator\CouponGenerationAmountValidator; use Sylius\Component\Promotion\Generator\GenerationPolicyInterface; -use Sylius\Component\Promotion\Generator\PromotionCouponGeneratorInstructionInterface; +use Sylius\Component\Promotion\Generator\ReadablePromotionCouponGeneratorInstructionInterface; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Context\ExecutionContextInterface; @@ -42,7 +42,7 @@ final class CouponGenerationAmountValidatorSpec extends ObjectBehavior function it_adds_violation( ExecutionContextInterface $context, - PromotionCouponGeneratorInstructionInterface $instruction, + ReadablePromotionCouponGeneratorInstructionInterface $instruction, GenerationPolicyInterface $generationPolicy, ): void { $constraint = new CouponPossibleGenerationAmount(); @@ -58,7 +58,7 @@ final class CouponGenerationAmountValidatorSpec extends ObjectBehavior function it_does_not_add_violation( ExecutionContextInterface $context, - PromotionCouponGeneratorInstructionInterface $instruction, + ReadablePromotionCouponGeneratorInstructionInterface $instruction, GenerationPolicyInterface $generationPolicy, ): void { $constraint = new CouponPossibleGenerationAmount(); diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionActionGroupValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionActionGroupValidatorSpec.php new file mode 100644 index 0000000000..318bfb7e3b --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionActionGroupValidatorSpec.php @@ -0,0 +1,84 @@ +beConstructedWith(['action_two' => ['Default', 'action_two']]); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_promotion_action_group( + Constraint $constraint, + PromotionActionInterface $promotionAction, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$promotionAction, $constraint]) + ; + } + + function it_throws_an_exception_if_value_is_not_an_instance_of_promotion_action(): void + { + $this + ->shouldThrow(UnexpectedValueException::class) + ->during('validate', [new \stdClass(), new PromotionActionGroup()]) + ; + } + + function it_calls_a_validator_with_group( + ExecutionContextInterface $context, + PromotionActionInterface $promotionAction, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ): void { + $promotionAction->getType()->willReturn('action_two'); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator->validate($promotionAction, null, ['Default', 'action_two'])->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($promotionAction, new PromotionActionGroup(['groups' => ['Default', 'test_group']])); + } + + function it_calls_validator_with_default_groups_if_none_provided_for_promotion_action_type( + ExecutionContextInterface $context, + PromotionActionInterface $promotionAction, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ): void { + $promotionAction->getType()->willReturn('action_one'); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator->validate($promotionAction, null, ['Default', 'test_group'])->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($promotionAction, new PromotionActionGroup(['groups' => ['Default', 'test_group']])); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionActionTypeValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionActionTypeValidatorSpec.php new file mode 100644 index 0000000000..4c2360c40e --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionActionTypeValidatorSpec.php @@ -0,0 +1,66 @@ +beConstructedWith(['action_one' => 'action_one', 'action_two' => 'action_two']); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_promotion_action_type( + Constraint $constraint, + PromotionActionInterface $promotionAction, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$promotionAction, $constraint]) + ; + } + + function it_throws_an_exception_if_value_is_not_an_instance_of_promotion_action(): void + { + $this + ->shouldThrow(UnexpectedValueException::class) + ->during('validate', [new \stdClass(), new PromotionActionType()]) + ; + } + + function it_adds_violation_if_promotion_action_has_invalid_type( + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + PromotionActionInterface $promotionAction, + ): void { + $promotionAction->getType()->willReturn('wrong_type'); + + $context->buildViolation('sylius.promotion_action.invalid_type')->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter('{{ available_action_types }}', 'action_one, action_two')->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->atPath('type')->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + + $this->validate($promotionAction, new PromotionActionType()); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionNotCouponBasedValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionNotCouponBasedValidatorSpec.php new file mode 100644 index 0000000000..b83b5229b0 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionNotCouponBasedValidatorSpec.php @@ -0,0 +1,111 @@ +initialize($context); + } + + function it_is_a_constraint_validator(): void + { + $this->shouldHaveType(ConstraintValidator::class); + } + + function it_does_nothing_when_value_is_null(ExecutionContextInterface $context): void + { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this->validate(null, new PromotionNotCouponBased()); + } + + function it_throws_an_exception_when_constraint_is_not_promotion_not_coupon_based( + ExecutionContextInterface $context, + PromotionCouponInterface $coupon, + Constraint $constraint, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$coupon, $constraint]) + ; + } + + function it_throws_an_exception_when_value_is_not_a_coupon(ExecutionContextInterface $context): void + { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $this + ->shouldThrow(UnexpectedValueException::class) + ->during('validate', [new \stdClass(), new PromotionNotCouponBased()]) + ; + } + + function it_does_nothing_when_coupon_has_no_promotion( + ExecutionContextInterface $context, + PromotionCouponInterface $coupon, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $coupon->getPromotion()->willReturn(null); + + $this->validate($coupon, new PromotionNotCouponBased()); + } + + function it_does_nothing_when_coupon_has_promotion_and_its_coupon_based( + ExecutionContextInterface $context, + PromotionCouponInterface $coupon, + PromotionInterface $promotion, + ): void { + $context->buildViolation(Argument::any())->shouldNotBeCalled(); + + $promotion->isCouponBased()->willReturn(true); + $coupon->getPromotion()->willReturn($promotion); + + $this->validate($coupon, new PromotionNotCouponBased()); + } + + function it_adds_violation_when_coupon_has_promotion_but_its_not_coupon_based( + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $violationBuilder, + PromotionCouponInterface $coupon, + PromotionInterface $promotion, + ): void { + $constraint = new PromotionNotCouponBased(); + + $context->buildViolation($constraint->message)->willReturn($violationBuilder); + $violationBuilder->atPath('promotion')->willReturn($violationBuilder); + $violationBuilder->addViolation()->shouldBeCalled(); + + $promotion->isCouponBased()->willReturn(false); + $coupon->getPromotion()->willReturn($promotion); + + $this->validate($coupon, $constraint); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionRuleGroupValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionRuleGroupValidatorSpec.php new file mode 100644 index 0000000000..12f668d56a --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionRuleGroupValidatorSpec.php @@ -0,0 +1,84 @@ +beConstructedWith(['rule_two' => ['Default', 'rule_two']]); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_promotion_rule_group( + Constraint $constraint, + PromotionRuleInterface $promotionRule, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$promotionRule, $constraint]) + ; + } + + function it_throws_an_exception_if_value_is_not_an_instance_of_array(): void + { + $this + ->shouldThrow(UnexpectedValueException::class) + ->during('validate', [new \stdClass(), new PromotionRuleGroup()]) + ; + } + + function it_calls_a_validator_with_group( + ExecutionContextInterface $context, + PromotionRuleInterface $promotionRule, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ): void { + $promotionRule->getType()->willReturn('rule_two'); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator->validate($promotionRule, null, ['Default', 'rule_two'])->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($promotionRule, new PromotionRuleGroup(['groups' => ['Default', 'test_group']])); + } + + function it_calls_validator_with_default_groups_if_none_provided_for_promotion_action_type( + ExecutionContextInterface $context, + PromotionRuleInterface $promotionRule, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ): void { + $promotionRule->getType()->willReturn('rule_one'); + + $context->getValidator()->willReturn($validator); + $validator->inContext($context)->willReturn($contextualValidator); + + $contextualValidator->validate($promotionRule, null, ['Default', 'test_group'])->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($promotionRule, new PromotionRuleGroup(['groups' => ['Default', 'test_group']])); + } +} diff --git a/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionRuleTypeValidatorSpec.php b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionRuleTypeValidatorSpec.php new file mode 100644 index 0000000000..a738614f98 --- /dev/null +++ b/src/Sylius/Bundle/PromotionBundle/spec/Validator/PromotionRuleTypeValidatorSpec.php @@ -0,0 +1,66 @@ +beConstructedWith(['rule_one' => 'rule_one', 'rule_two' => 'rule_two']); + + $this->initialize($context); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_promotion_rule_type( + Constraint $constraint, + PromotionRuleInterface $promotionRule, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$promotionRule, $constraint]) + ; + } + + function it_throws_an_exception_if_value_is_not_an_instance_of_array(): void + { + $this + ->shouldThrow(UnexpectedValueException::class) + ->during('validate', [new \stdClass(), new PromotionRuleType()]) + ; + } + + function it_adds_violation_if_promotion_rule_has_invalid_type( + ExecutionContextInterface $context, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + PromotionRuleInterface $promotionRule, + ): void { + $promotionRule->getType()->willReturn('wrong_type'); + + $context->buildViolation('sylius.promotion_rule.invalid_type')->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter('{{ available_rule_types }}', 'rule_one, rule_two')->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->atPath('type')->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + + $this->validate($promotionRule, new PromotionRuleType()); + } +} diff --git a/src/Sylius/Bundle/ReviewBundle/DependencyInjection/Compiler/RegisterReviewFactoryPass.php b/src/Sylius/Bundle/ReviewBundle/DependencyInjection/Compiler/RegisterReviewFactoryPass.php index 8334baa2f7..1411a1bc2a 100644 --- a/src/Sylius/Bundle/ReviewBundle/DependencyInjection/Compiler/RegisterReviewFactoryPass.php +++ b/src/Sylius/Bundle/ReviewBundle/DependencyInjection/Compiler/RegisterReviewFactoryPass.php @@ -28,7 +28,7 @@ final class RegisterReviewFactoryPass implements CompilerPassInterface $reviewFactoryDefinition = new Definition(ReviewFactory::class, [$factory]); $reviewFactoryDefinition->setPublic(true); - $container->setDefinition(sprintf('sylius.factory.' . $subject . '_review'), $reviewFactoryDefinition); + $container->setDefinition(sprintf('sylius.factory.%s_review', $subject), $reviewFactoryDefinition); } } } diff --git a/src/Sylius/Bundle/ReviewBundle/Doctrine/ORM/Subscriber/LoadMetadataSubscriber.php b/src/Sylius/Bundle/ReviewBundle/Doctrine/ORM/Subscriber/LoadMetadataSubscriber.php index 760ddf3cad..d68a6ec80a 100644 --- a/src/Sylius/Bundle/ReviewBundle/Doctrine/ORM/Subscriber/LoadMetadataSubscriber.php +++ b/src/Sylius/Bundle/ReviewBundle/Doctrine/ORM/Subscriber/LoadMetadataSubscriber.php @@ -38,16 +38,22 @@ final class LoadMetadataSubscriber implements EventSubscriber foreach ($this->subjects as $subject => $class) { if ($class['review']['classes']['model'] === $metadata->getName()) { - $reviewableEntity = $class['subject']; - $reviewerEntity = $class['reviewer']['classes']['model']; - $reviewableEntityMetadata = $metadataFactory->getMetadataFor($reviewableEntity); - $reviewerEntityMetadata = $metadataFactory->getMetadataFor($reviewerEntity); + if (!$metadata->hasAssociation('reviewSubject')) { + $reviewableEntity = $class['subject']; + $reviewableEntityMetadata = $metadataFactory->getMetadataFor($reviewableEntity); - $metadata->mapManyToOne($this->createSubjectMapping($reviewableEntity, $subject, $reviewableEntityMetadata)); - $metadata->mapManyToOne($this->createReviewerMapping($reviewerEntity, $reviewerEntityMetadata)); + $metadata->mapManyToOne($this->createSubjectMapping($reviewableEntity, $subject, $reviewableEntityMetadata)); + } + + if (!$metadata->hasAssociation('author')) { + $reviewerEntity = $class['reviewer']['classes']['model']; + $reviewerEntityMetadata = $metadataFactory->getMetadataFor($reviewerEntity); + + $metadata->mapManyToOne($this->createReviewerMapping($reviewerEntity, $reviewerEntityMetadata)); + } } - if ($class['subject'] === $metadata->getName()) { + if ($class['subject'] === $metadata->getName() && !$metadata->hasAssociation('reviews')) { $reviewEntity = $class['review']['classes']['model']; $metadata->mapOneToMany($this->createReviewsMapping($reviewEntity)); diff --git a/src/Sylius/Bundle/ReviewBundle/composer.json b/src/Sylius/Bundle/ReviewBundle/composer.json index a9beab7095..26b3e271bf 100644 --- a/src/Sylius/Bundle/ReviewBundle/composer.json +++ b/src/Sylius/Bundle/ReviewBundle/composer.json @@ -38,13 +38,13 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "stof/doctrine-extensions-bundle": "^1.4", "sylius/mailer-bundle": "^1.8 || ^2.0@beta", "sylius/resource-bundle": "^1.9", - "sylius/review": "^1.12", - "sylius/user-bundle": "^1.12", - "symfony/framework-bundle": "^5.4 || ^6.0" + "sylius/review": "^1.13", + "sylius/user-bundle": "^1.13", + "symfony/framework-bundle": "^5.4.21 || ^6.4" }, "conflict": { "stof/doctrine-extensions-bundle": "1.8.0", @@ -52,13 +52,14 @@ }, "require-dev": { "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0", - "symfony/security-bundle": "^5.4 || ^6.0", - "symfony/validator": "^5.4 || ^6.0" + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4", + "symfony/security-bundle": "^5.4.21 || ^6.4", + "symfony/validator": "^5.4.21 || ^6.4" }, "config": { "allow-plugins": { @@ -67,7 +68,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { diff --git a/src/Sylius/Bundle/ReviewBundle/phpunit.xml.dist b/src/Sylius/Bundle/ReviewBundle/phpunit.xml.dist index 5cf3d31445..d058334ec8 100644 --- a/src/Sylius/Bundle/ReviewBundle/phpunit.xml.dist +++ b/src/Sylius/Bundle/ReviewBundle/phpunit.xml.dist @@ -11,7 +11,6 @@ ./test/ - ./Tests/ diff --git a/src/Sylius/Bundle/ReviewBundle/spec/Doctrine/ORM/Subscriber/LoadMetadataSubscriberSpec.php b/src/Sylius/Bundle/ReviewBundle/spec/Doctrine/ORM/Subscriber/LoadMetadataSubscriberSpec.php index 61ad376b85..7e07a1dd49 100644 --- a/src/Sylius/Bundle/ReviewBundle/spec/Doctrine/ORM/Subscriber/LoadMetadataSubscriberSpec.php +++ b/src/Sylius/Bundle/ReviewBundle/spec/Doctrine/ORM/Subscriber/LoadMetadataSubscriberSpec.php @@ -66,7 +66,10 @@ final class LoadMetadataSubscriberSpec extends ObjectBehavior $classMetadataInfo->fieldMappings = ['id' => ['columnName' => 'id']]; $metadataFactory->getMetadataFor('AcmeBundle\Entity\ReviewableModel')->willReturn($classMetadataInfo); $metadataFactory->getMetadataFor('AcmeBundle\Entity\ReviewerModel')->willReturn($classMetadataInfo); + $metadata->getName()->willReturn('AcmeBundle\Entity\ReviewModel'); + $metadata->hasAssociation('reviewSubject')->willReturn(false); + $metadata->hasAssociation('author')->willReturn(false); $metadata->mapManyToOne([ 'fieldName' => 'reviewSubject', @@ -95,6 +98,25 @@ final class LoadMetadataSubscriberSpec extends ObjectBehavior $this->loadClassMetadata($eventArguments); } + function it_does_not_map_relation_for_review_model_if_the_relation_already_exists( + ClassMetadataFactory $metadataFactory, + ClassMetadata $metadata, + EntityManager $entityManager, + LoadClassMetadataEventArgs $eventArguments, + ): void { + $eventArguments->getClassMetadata()->willReturn($metadata); + $eventArguments->getEntityManager()->willReturn($entityManager); + $entityManager->getMetadataFactory()->willReturn($metadataFactory); + + $metadata->getName()->willReturn('AcmeBundle\Entity\ReviewModel'); + $metadata->hasAssociation('reviewSubject')->willReturn(true); + $metadata->hasAssociation('author')->willReturn(true); + + $metadata->mapManyToOne(Argument::any())->shouldNotBeCalled(); + + $this->loadClassMetadata($eventArguments); + } + function it_maps_proper_relations_for_reviewable_model( ClassMetadataFactory $metadataFactory, ClassMetadata $metadata, @@ -104,7 +126,9 @@ final class LoadMetadataSubscriberSpec extends ObjectBehavior $eventArguments->getClassMetadata()->willReturn($metadata); $eventArguments->getEntityManager()->willReturn($entityManager); $entityManager->getMetadataFactory()->willReturn($metadataFactory); + $metadata->getName()->willReturn('AcmeBundle\Entity\ReviewableModel'); + $metadata->hasAssociation('reviews')->willReturn(false); $metadata->mapOneToMany([ 'fieldName' => 'reviews', @@ -116,7 +140,25 @@ final class LoadMetadataSubscriberSpec extends ObjectBehavior $this->loadClassMetadata($eventArguments); } - function it_skips_mapping_configuration_if_metadata_name_is_not_different( + function it_does_not_map_relations_for_reviewable_model_if_the_relation_already_exists( + ClassMetadataFactory $metadataFactory, + ClassMetadata $metadata, + EntityManager $entityManager, + LoadClassMetadataEventArgs $eventArguments, + ): void { + $eventArguments->getClassMetadata()->willReturn($metadata); + $eventArguments->getEntityManager()->willReturn($entityManager); + $entityManager->getMetadataFactory()->willReturn($metadataFactory); + + $metadata->getName()->willReturn('AcmeBundle\Entity\ReviewableModel'); + $metadata->hasAssociation('reviews')->willReturn(true); + + $metadata->mapOneToMany(Argument::any())->shouldNotBeCalled(); + + $this->loadClassMetadata($eventArguments); + } + + function it_skips_mapping_configuration_if_metadata_name_is_different( ClassMetadataFactory $metadataFactory, ClassMetadata $metadata, EntityManager $entityManager, diff --git a/src/Sylius/Bundle/ReviewBundle/test/app/config/config.yml b/src/Sylius/Bundle/ReviewBundle/test/app/config/config.yml index 634bbe27b6..a7718bba16 100644 --- a/src/Sylius/Bundle/ReviewBundle/test/app/config/config.yml +++ b/src/Sylius/Bundle/ReviewBundle/test/app/config/config.yml @@ -50,7 +50,7 @@ security: password: foo roles: ROLE_USER password_hashers: - Sylius\Component\User\Model\UserInterface: sha512 + Sylius\Component\User\Model\UserInterface: plaintext firewalls: admin: switch_user: true diff --git a/src/Sylius/Bundle/ShippingBundle/Assigner/ShippingDateAssigner.php b/src/Sylius/Bundle/ShippingBundle/Assigner/ShippingDateAssigner.php index ac531c1643..d333f6fbc6 100644 --- a/src/Sylius/Bundle/ShippingBundle/Assigner/ShippingDateAssigner.php +++ b/src/Sylius/Bundle/ShippingBundle/Assigner/ShippingDateAssigner.php @@ -22,9 +22,13 @@ final class ShippingDateAssigner implements ShippingDateAssignerInterface public function __construct(private DateTimeProvider|DateTimeProviderInterface $calendar) { if ($calendar instanceof DateTimeProvider) { - @trigger_error( - sprintf('Passing a "Sylius\Bundle\ShippingBundle\Provider\DateTimeProvider" to "%s" constructor is deprecated since Sylius 1.11 and will be prohibited in 2.0. Use "Sylius\Calendar\Provider\DateTimeProviderInterface" instead.', self::class), - \E_USER_DEPRECATED, + trigger_deprecation( + 'sylius/shipping-bundle', + '1.11', + 'Passing a "%s" to "%s" constructor is deprecated and will be prohibited in Sylius 2.0. Use "%s" instead.', + DateTimeProvider::class, + self::class, + DateTimeProviderInterface::class, ); } } diff --git a/src/Sylius/Bundle/ShippingBundle/Attribute/AsShippingCalculator.php b/src/Sylius/Bundle/ShippingBundle/Attribute/AsShippingCalculator.php new file mode 100644 index 0000000000..5e491d0fbd --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Attribute/AsShippingCalculator.php @@ -0,0 +1,48 @@ +calculator; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getFormType(): string + { + return $this->formType; + } + + public function getPriority(): int + { + return $this->priority; + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/Attribute/AsShippingMethodResolver.php b/src/Sylius/Bundle/ShippingBundle/Attribute/AsShippingMethodResolver.php new file mode 100644 index 0000000000..bed7532990 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Attribute/AsShippingMethodResolver.php @@ -0,0 +1,42 @@ +type; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getPriority(): int + { + return $this->priority; + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/Attribute/AsShippingMethodRuleChecker.php b/src/Sylius/Bundle/ShippingBundle/Attribute/AsShippingMethodRuleChecker.php new file mode 100644 index 0000000000..39d8f3b89a --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Attribute/AsShippingMethodRuleChecker.php @@ -0,0 +1,48 @@ +type; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getFormType(): string + { + return $this->formType; + } + + public function getPriority(): int + { + return $this->priority; + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php index 70728065e8..b6340f8007 100644 --- a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php +++ b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/Configuration.php @@ -49,6 +49,24 @@ final class Configuration implements ConfigurationInterface $rootNode ->addDefaultsIfNotSet() ->children() + ->arrayNode('shipping_method_rule') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->end() + ->end() + ->arrayNode('shipping_method_calculator') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('validation_groups') + ->useAttributeAsKey('name') + ->variablePrototype()->end() + ->end() + ->end() + ->end() ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() ->end() ; diff --git a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php index 9f2f80d065..4166bb3262 100644 --- a/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php +++ b/src/Sylius/Bundle/ShippingBundle/DependencyInjection/SyliusShippingExtension.php @@ -14,7 +14,11 @@ declare(strict_types=1); namespace Sylius\Bundle\ShippingBundle\DependencyInjection; use Sylius\Bundle\ResourceBundle\DependencyInjection\Extension\AbstractResourceExtension; +use Sylius\Bundle\ShippingBundle\Attribute\AsShippingCalculator; +use Sylius\Bundle\ShippingBundle\Attribute\AsShippingMethodResolver; +use Sylius\Bundle\ShippingBundle\Attribute\AsShippingMethodRuleChecker; use Symfony\Component\Config\FileLocator; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -29,6 +33,48 @@ final class SyliusShippingExtension extends AbstractResourceExtension $this->registerResources('sylius', $config['driver'], $config['resources'], $container); + $container->setParameter('sylius.shipping.shipping_method_rule.validation_groups', $config['shipping_method_rule']['validation_groups']); + $container->setParameter('sylius.shipping.shipping_method_calculator.validation_groups', $config['shipping_method_calculator']['validation_groups']); + $loader->load('services.xml'); + $this->registerAutoconfiguration($container); + } + + private function registerAutoconfiguration(ContainerBuilder $container): void + { + $container->registerAttributeForAutoconfiguration( + AsShippingCalculator::class, + static function (ChildDefinition $definition, AsShippingCalculator $attribute): void { + $definition->addTag(AsShippingCalculator::SERVICE_TAG, [ + 'calculator' => $attribute->getCalculator(), + 'label' => $attribute->getLabel(), + 'form-type' => $attribute->getFormType(), + 'priority' => $attribute->getPriority(), + ]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsShippingMethodResolver::class, + static function (ChildDefinition $definition, AsShippingMethodResolver $attribute): void { + $definition->addTag(AsShippingMethodResolver::SERVICE_TAG, [ + 'type' => $attribute->getType(), + 'label' => $attribute->getLabel(), + 'priority' => $attribute->getPriority(), + ]); + }, + ); + + $container->registerAttributeForAutoconfiguration( + AsShippingMethodRuleChecker::class, + static function (ChildDefinition $definition, AsShippingMethodRuleChecker $attribute): void { + $definition->addTag(AsShippingMethodRuleChecker::SERVICE_TAG, [ + 'type' => $attribute->getType(), + 'label' => $attribute->getLabel(), + 'form-type' => $attribute->getFormType(), + 'priority' => $attribute->getPriority(), + ]); + }, + ); } } diff --git a/src/Sylius/Bundle/ShippingBundle/Doctrine/ORM/ShippingMethodRepository.php b/src/Sylius/Bundle/ShippingBundle/Doctrine/ORM/ShippingMethodRepository.php index c59ef0d88e..9cb68c157d 100644 --- a/src/Sylius/Bundle/ShippingBundle/Doctrine/ORM/ShippingMethodRepository.php +++ b/src/Sylius/Bundle/ShippingBundle/Doctrine/ORM/ShippingMethodRepository.php @@ -14,8 +14,14 @@ declare(strict_types=1); namespace Sylius\Bundle\ShippingBundle\Doctrine\ORM; use Sylius\Bundle\ResourceBundle\Doctrine\ORM\EntityRepository; +use Sylius\Component\Shipping\Model\ShippingMethodInterface; use Sylius\Component\Shipping\Repository\ShippingMethodRepositoryInterface; +/** + * @template T of ShippingMethodInterface + * + * @implements ShippingMethodRepositoryInterface + */ class ShippingMethodRepository extends EntityRepository implements ShippingMethodRepositoryInterface { public function findByName(string $name, string $locale): array @@ -30,4 +36,16 @@ class ShippingMethodRepository extends EntityRepository implements ShippingMetho ->getResult() ; } + + public function findEnabledWithRules(): array + { + return $this->createQueryBuilder('o') + ->addSelect('rules') + ->leftJoin('o.rules', 'rules') + ->andWhere('o.enabled = :enabled') + ->setParameter('enabled', true) + ->getQuery() + ->getResult() + ; + } } diff --git a/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/FlatRateConfigurationType.php b/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/FlatRateConfigurationType.php index fd4f3b5b3a..5de03df6d1 100644 --- a/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/FlatRateConfigurationType.php +++ b/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/FlatRateConfigurationType.php @@ -17,9 +17,6 @@ use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Type; final class FlatRateConfigurationType extends AbstractType { @@ -28,11 +25,6 @@ final class FlatRateConfigurationType extends AbstractType $builder ->add('amount', MoneyType::class, [ 'label' => 'sylius.form.shipping_calculator.flat_rate_configuration.amount', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Range(['min' => 0, 'minMessage' => 'sylius.shipping_method.calculator.min', 'groups' => ['sylius']]), - new Type(['type' => 'integer', 'groups' => ['sylius']]), - ], 'currency' => $options['currency'], ]) ; diff --git a/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/PerUnitRateConfigurationType.php b/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/PerUnitRateConfigurationType.php index 239dcda8f9..3fc36ff8c0 100644 --- a/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/PerUnitRateConfigurationType.php +++ b/src/Sylius/Bundle/ShippingBundle/Form/Type/Calculator/PerUnitRateConfigurationType.php @@ -17,9 +17,6 @@ use Sylius\Bundle\MoneyBundle\Form\Type\MoneyType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Range; -use Symfony\Component\Validator\Constraints\Type; final class PerUnitRateConfigurationType extends AbstractType { @@ -28,11 +25,6 @@ final class PerUnitRateConfigurationType extends AbstractType $builder ->add('amount', MoneyType::class, [ 'label' => 'sylius.form.shipping_calculator.per_unit_rate_configuration.amount', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Range(['min' => 0, 'minMessage' => 'sylius.shipping_method.calculator.min', 'groups' => ['sylius']]), - new Type(['type' => 'integer', 'groups' => ['sylius']]), - ], 'currency' => $options['currency'], ]) ; diff --git a/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightGreaterThanOrEqualConfigurationType.php b/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightGreaterThanOrEqualConfigurationType.php index 4ce271baa2..7d0fd0be7f 100644 --- a/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightGreaterThanOrEqualConfigurationType.php +++ b/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightGreaterThanOrEqualConfigurationType.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\ShippingBundle\Form\Type\Rule; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\GreaterThan; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class TotalWeightGreaterThanOrEqualConfigurationType extends AbstractType { @@ -29,11 +26,6 @@ final class TotalWeightGreaterThanOrEqualConfigurationType extends AbstractType NumberType::class, [ 'label' => 'sylius.form.shipping_method_rule.weight', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - new GreaterThan(['value' => 0, 'groups' => ['sylius']]), - ], ], ); } diff --git a/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightLessThanOrEqualConfigurationType.php b/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightLessThanOrEqualConfigurationType.php index 34d962fcc1..b826dff6aa 100644 --- a/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightLessThanOrEqualConfigurationType.php +++ b/src/Sylius/Bundle/ShippingBundle/Form/Type/Rule/TotalWeightLessThanOrEqualConfigurationType.php @@ -16,9 +16,6 @@ namespace Sylius\Bundle\ShippingBundle\Form\Type\Rule; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Validator\Constraints\GreaterThan; -use Symfony\Component\Validator\Constraints\NotBlank; -use Symfony\Component\Validator\Constraints\Type; final class TotalWeightLessThanOrEqualConfigurationType extends AbstractType { @@ -29,11 +26,6 @@ final class TotalWeightLessThanOrEqualConfigurationType extends AbstractType NumberType::class, [ 'label' => 'sylius.form.shipping_method_rule.weight', - 'constraints' => [ - new NotBlank(['groups' => ['sylius']]), - new Type(['type' => 'numeric', 'groups' => ['sylius']]), - new GreaterThan(['value' => 0, 'groups' => ['sylius']]), - ], ], ); } diff --git a/src/Sylius/Bundle/ShippingBundle/Provider/Calendar.php b/src/Sylius/Bundle/ShippingBundle/Provider/Calendar.php index 8b174c670a..6c53231640 100644 --- a/src/Sylius/Bundle/ShippingBundle/Provider/Calendar.php +++ b/src/Sylius/Bundle/ShippingBundle/Provider/Calendar.php @@ -13,11 +13,17 @@ declare(strict_types=1); namespace Sylius\Bundle\ShippingBundle\Provider; -@trigger_error( - 'The "Sylius\Bundle\ShippingBundle\Provider\Calendar" class is deprecated since Sylius 1.11 and will be removed in 2.0. Use "Sylius\Calendar\Provider\Calendar" instead.', - \E_USER_DEPRECATED, +trigger_deprecation( + 'sylius/shipping-bundle', + '1.11', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + Calendar::class, + 'Symfony\Component\Clock\Clock', ); +/** + * @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see 'Symfony\Component\Clock\Clock'} instead. + */ final class Calendar implements DateTimeProvider { public function today(): \DateTimeInterface diff --git a/src/Sylius/Bundle/ShippingBundle/Provider/DateTimeProvider.php b/src/Sylius/Bundle/ShippingBundle/Provider/DateTimeProvider.php index dc056425f4..dc3bc2f660 100644 --- a/src/Sylius/Bundle/ShippingBundle/Provider/DateTimeProvider.php +++ b/src/Sylius/Bundle/ShippingBundle/Provider/DateTimeProvider.php @@ -13,6 +13,17 @@ declare(strict_types=1); namespace Sylius\Bundle\ShippingBundle\Provider; +trigger_deprecation( + 'sylius/shipping-bundle', + '1.13', + 'The "%s" interface is deprecated and will be removed in Sylius 2.0. Use "%s" instead.', + DateTimeProvider::class, + 'Symfony\Component\Clock\Clock', +); + +/** + * @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see 'Symfony\Component\Clock\Clock'} instead. + */ interface DateTimeProvider { public function today(): \DateTimeInterface; diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/app/config.yml b/src/Sylius/Bundle/ShippingBundle/Resources/config/app/config.yml index 9154767fe5..d9fc4421c2 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/config/app/config.yml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/app/config.yml @@ -7,3 +7,13 @@ jms_serializer: sylius-shipping: namespace_prefix: "Sylius\\Component\\Shipping" path: "@SyliusShippingBundle/Resources/config/serializer" + +sylius_shipping: + shipping_method_rule: + validation_groups: + total_weight_greater_than_or_equal: + - 'sylius' + - 'sylius_shipping_method_rule_total_weight' + total_weight_less_than_or_equal: + - 'sylius' + - 'sylius_shipping_method_rule_total_weight' diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/services.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/services.xml index a3220e93a3..8f465bf463 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/config/services.xml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/services.xml @@ -14,9 +14,7 @@ - - - + @@ -73,9 +71,7 @@ label="sylius.form.shipping_calculator.per_unit_rate_configuration.label"/> - - The "%service_id%" is deprecated since Sylius 1.11 and will be removed in 2.0. Use "Sylius\Calendar\Provider\DateTimeProviderInterface" instead. - + diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/services/validator.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/services/validator.xml new file mode 100644 index 0000000000..1139556a99 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/services/validator.xml @@ -0,0 +1,34 @@ + + + + + + + + %sylius.shipping_calculators% + + + + + %sylius.shipping_method_rules% + %sylius.shipping.shipping_method_rule.validation_groups% + + + + + %sylius.shipping.shipping_method_calculator.validation_groups% + + + diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml index 62d19a9ee0..95c84bbcf7 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethod.xml @@ -34,6 +34,14 @@ + + + +
+ + diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodRule.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodRule.xml new file mode 100644 index 0000000000..a9770467dd --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodRule.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodTranslation.xml b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodTranslation.xml index 3998e30849..2732c1d39b 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodTranslation.xml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/validation/ShippingMethodTranslation.xml @@ -13,6 +13,14 @@ + + + + + @@ -26,5 +34,15 @@ + + + + + + + + + + diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/config/workflow/state_machine.yaml b/src/Sylius/Bundle/ShippingBundle/Resources/config/workflow/state_machine.yaml new file mode 100644 index 0000000000..289b675db0 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Resources/config/workflow/state_machine.yaml @@ -0,0 +1,25 @@ +framework: + workflows: + !php/const Sylius\Component\Shipping\ShipmentTransitions::GRAPH: + type: state_machine + marking_store: + type: method + property: shippingState + supports: + - Sylius\Component\Shipping\Model\ShipmentInterface + initial_marking: !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_CART + places: + - !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_CART + - !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_READY + - !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_SHIPPED + - !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_CANCELLED + transitions: + !php/const Sylius\Component\Shipping\ShipmentTransitions::TRANSITION_CREATE: + from: !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_CART + to: !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_READY + !php/const Sylius\Component\Shipping\ShipmentTransitions::TRANSITION_SHIP: + from: !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_READY + to: !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_SHIPPED + !php/const Sylius\Component\Shipping\ShipmentTransitions::TRANSITION_CANCEL: + from: !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_READY + to: !php/const Sylius\Component\Shipping\Model\ShipmentInterface::STATE_CANCELLED diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml b/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml index 9da6ebf270..725c59fedf 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.en.yml @@ -16,6 +16,7 @@ sylius: calculator: min: 'Shipping charge cannot be lower than 0.' not_blank: 'Please select shipping method calculator.' + invalid: 'Invalid calculator. Available calculators are {{ available_calculators }}.' name: max_length: 'Shipping method name must not be longer than {{ limit }} characters.' min_length: 'Shipping method name must be at least {{ limit }} characters long.' @@ -26,7 +27,14 @@ sylius: unique: 'The shipping method with given code already exists.' zone: not_blank: 'Please select shipping method zone.' + rule: + invalid_type: 'Invalid rule type. Available rule types are {{ available_rule_types }}.' shipment: shipping_method: not_blank: 'Please select shipping method.' + translation: + locale: + not_blank: 'Please enter the locale.' + invalid: 'This value is not a valid locale.' + unique: 'A translation for the {{ value }} locale code already exists.' diff --git a/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.fr.yml b/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.fr.yml index 079f267b09..8b99b5185d 100644 --- a/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.fr.yml +++ b/src/Sylius/Bundle/ShippingBundle/Resources/translations/validators.fr.yml @@ -15,6 +15,7 @@ sylius: calculator: min: 'Les frais de port ne peuvent pas être inférieurs à 0.' not_blank: 'Veuillez sélectionner une méthode de calcul pour la livraison.' + invalid: 'Calculateur invalide. Les calculateurs disponibles sont {{ available_calculators }}.' name: max_length: 'Le nom de la méthode de livraison ne peut dépasser les {{ limit }} caractères.' min_length: 'Le nom de la méthode de livraison doit contenir au moins {{ limit }} caractères.' @@ -25,6 +26,13 @@ sylius: unique: 'Ce code de mode de livraison existe déjà.' zone: not_blank: 'S''il vous plaît sélectionnez zone de méthode de livraison.' + rule: + invalid_type: 'Le type de règle n''est pas valide. Les types de règles disponibles sont {{ available_rule_types }}.' shipment: shipping_method: not_blank: 'Veuillez sélectionner un mode de livraison.' + translation: + locale: + not_blank: 'Vous devez entrer une valeur.' + invalid: 'Ce paramètre régional n''est pas valide.' + unique: 'Une traduction pour la locale {{ value }} existe déjà.' diff --git a/src/Sylius/Bundle/ShippingBundle/Tests/DependencyInjection/SyliusShippingExtensionTest.php b/src/Sylius/Bundle/ShippingBundle/Tests/DependencyInjection/SyliusShippingExtensionTest.php new file mode 100644 index 0000000000..71b331d9c7 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Tests/DependencyInjection/SyliusShippingExtensionTest.php @@ -0,0 +1,142 @@ +container->setDefinition( + 'acme.shipping_calculator_autoconfigured', + (new Definition()) + ->setClass(ShippingCalculatorStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.shipping_calculator_autoconfigured', + AsShippingCalculator::SERVICE_TAG, + [ + 'calculator' => 'test', + 'label' => 'Test', + 'form-type' => 'SomeFormType', + 'priority' => 0, + ], + ); + } + + /** @test */ + public function it_autoconfigures_shipping_method_resolver_with_attribute(): void + { + $this->container->setDefinition( + 'acme.shipping_method_resolver_autoconfigured', + (new Definition()) + ->setClass(ShippingMethodResolverStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.shipping_method_resolver_autoconfigured', + AsShippingMethodResolver::SERVICE_TAG, + [ + 'type' => 'test', + 'label' => 'Test', + 'priority' => 10, + ], + ); + } + + /** @test */ + public function it_autoconfigures_shipping_method_rule_checker_with_attribute(): void + { + $this->container->setDefinition( + 'acme.shipping_method_rule_checker_autoconfigured', + (new Definition()) + ->setClass(ShippingMethodRuleCheckerStub::class) + ->setAutoconfigured(true), + ); + + $this->load(); + $this->compile(); + + $this->assertContainerBuilderHasServiceDefinitionWithTag( + 'acme.shipping_method_rule_checker_autoconfigured', + AsShippingMethodRuleChecker::SERVICE_TAG, + [ + 'type' => 'test', + 'label' => 'Test', + 'form-type' => 'SomeFormType', + 'priority' => 20, + ], + ); + } + + /** @test */ + public function it_loads_shipping_method_rules_validation_groups_parameter_value_properly(): void + { + $this->load(['shipping_method_rule' => [ + 'validation_groups' => [ + 'total_weight_greater_than_or_equal' => ['sylius', 'sylius_shipping_method_rule_total_weight'], + 'order_total_greater_than_or_equal' => ['sylius'], + 'order_total_less_than_or_equal' => ['sylius', 'sylius_shipping_method_rule_order_total'], + 'total_weight_less_than_or_equal' => ['sylius'], + ], + ]]); + + $this->assertContainerBuilderHasParameter('sylius.shipping.shipping_method_rule.validation_groups', [ + 'total_weight_greater_than_or_equal' => ['sylius', 'sylius_shipping_method_rule_total_weight'], + 'order_total_greater_than_or_equal' => ['sylius'], + 'order_total_less_than_or_equal' => ['sylius', 'sylius_shipping_method_rule_order_total'], + 'total_weight_less_than_or_equal' => ['sylius'], + ]); + } + + /** @test */ + public function it_loads_shipping_method_calculators_validation_groups_parameter_value_properly(): void + { + $this->load(['shipping_method_calculator' => [ + 'validation_groups' => [ + 'flat_rate' => ['sylius'], + 'per_unit_rate' => ['sylius', 'sylius_per_unit_rate'], + ], + ]]); + + $this->assertContainerBuilderHasParameter('sylius.shipping.shipping_method_calculator.validation_groups', [ + 'flat_rate' => ['sylius'], + 'per_unit_rate' => ['sylius', 'sylius_per_unit_rate'], + ]); + } + + protected function getContainerExtensions(): array + { + return [new SyliusShippingExtension()]; + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/Tests/Stub/ShippingCalculatorStub.php b/src/Sylius/Bundle/ShippingBundle/Tests/Stub/ShippingCalculatorStub.php new file mode 100644 index 0000000000..3adbe6b5f8 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Tests/Stub/ShippingCalculatorStub.php @@ -0,0 +1,32 @@ +> $validationGroups */ + public function __construct(private array $validationGroups) + { + } + + /** + * @param FormInterface|ShippingMethodInterface $object + * + * @return array + */ + public function __invoke($object): array + { + if ($object instanceof FormInterface) { + $object = $object->getData(); + } + + Assert::isInstanceOf($object, ShippingMethodInterface::class); + + return $this->validationGroups[$object->getCalculator()] ?? ['sylius']; + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodCalculatorExistsValidator.php b/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodCalculatorExistsValidator.php new file mode 100644 index 0000000000..81757148a5 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodCalculatorExistsValidator.php @@ -0,0 +1,46 @@ + $calculators */ + public function __construct(private array $calculators) + { + } + + /** @param string|null $value */ + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof ShippingMethodCalculatorExists) { + throw new UnexpectedTypeException($constraint, ShippingMethodCalculatorExists::class); + } + + if ($value === null || $value === '') { + return; + } + + if (!in_array($value, array_keys($this->calculators), true)) { + $this->context->buildViolation($constraint->invalidShippingCalculator) + ->setParameter('{{ available_calculators }}', implode(', ', array_keys($this->calculators))) + ->addViolation() + ; + } + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodRuleValidator.php b/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodRuleValidator.php new file mode 100644 index 0000000000..b0b832613a --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/Validator/ShippingMethodRuleValidator.php @@ -0,0 +1,61 @@ + $ruleTypes + * @param array> $validationGroups + */ + public function __construct( + private array $ruleTypes, + private array $validationGroups, + ) { + } + + public function validate(mixed $value, Constraint $constraint): void + { + if (!$value instanceof ShippingMethodRuleInterface) { + throw new UnexpectedValueException($value, ShippingMethodRuleInterface::class); + } + + if (!$constraint instanceof ShippingMethodRule) { + throw new UnexpectedTypeException($constraint, ShippingMethodRule::class); + } + + $type = $value->getType(); + if (!array_key_exists($type, $this->ruleTypes)) { + $this->context->buildViolation($constraint->invalidType) + ->setParameter('{{ available_rule_types }}', implode(', ', array_keys($this->ruleTypes))) + ->atPath('type') + ->addViolation() + ; + + return; + } + + /** @var string[] $groups */ + $groups = $this->validationGroups[$type] ?? $constraint->groups; + $validator = $this->context->getValidator()->inContext($this->context); + $validator->validate(value: $value, groups: $groups); + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/composer.json b/src/Sylius/Bundle/ShippingBundle/composer.json index ee07752d0b..4e8500200d 100644 --- a/src/Sylius/Bundle/ShippingBundle/composer.json +++ b/src/Sylius/Bundle/ShippingBundle/composer.json @@ -28,13 +28,13 @@ } ], "require": { - "php": "^8.0", + "php": "^8.1", "stof/doctrine-extensions-bundle": "^1.4", - "sylius/calendar": "^0.3 || ^0.4", - "sylius/money-bundle": "^1.12", + "sylius/calendar": "^0.3 || ^0.4 || ^0.5", + "sylius/money-bundle": "^1.13", "sylius/resource-bundle": "^1.9", - "sylius/shipping": "^1.12", - "symfony/framework-bundle": "^5.4 || ^6.0" + "sylius/shipping": "^1.13", + "symfony/framework-bundle": "^5.4.21 || ^6.4" }, "conflict": { "doctrine/orm": "2.15.2", @@ -45,12 +45,12 @@ "doctrine/orm": "^2.13", "matthiasnoback/symfony-dependency-injection-test": "^4.2", "phpspec/phpspec": "^7.2", - "phpunit/phpunit": "^8.5", + "phpunit/phpunit": "^9.5", "polishsymfonycommunity/symfony-mocker-container": "^1.0", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/dependency-injection": "^5.4 || ^6.0", - "symfony/form": "^5.4 || ^6.0", - "symfony/validator": "^5.4 || ^6.0" + "symfony/browser-kit": "^5.4.21 || ^6.4", + "symfony/dependency-injection": "^5.4.21 || ^6.4", + "symfony/form": "^5.4.21 || ^6.4", + "symfony/validator": "^5.4.21 || ^6.4" }, "config": { "allow-plugins": { @@ -59,7 +59,7 @@ }, "extra": { "branch-alias": { - "dev-main": "1.12-dev" + "dev-main": "1.13-dev" } }, "autoload": { @@ -70,7 +70,10 @@ "autoload-dev": { "classmap": [ "test/app/AppKernel.php" - ] + ], + "psr-4": { + "Sylius\\Bundle\\ShippingBundle\\Tests\\": "Tests/" + } }, "repositories": [ { diff --git a/src/Sylius/Bundle/ShippingBundle/spec/Validator/GroupsGenerator/ShippingMethodConfigurationGroupsGeneratorSpec.php b/src/Sylius/Bundle/ShippingBundle/spec/Validator/GroupsGenerator/ShippingMethodConfigurationGroupsGeneratorSpec.php new file mode 100644 index 0000000000..f9fd2028af --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/spec/Validator/GroupsGenerator/ShippingMethodConfigurationGroupsGeneratorSpec.php @@ -0,0 +1,63 @@ +beConstructedWith([ + 'flat_rate' => ['rate', 'sylius'], + 'per_unit_rate' => ['rate', 'sylius'], + ]); + } + + function it_throws_error_if_invalid_object_is_passed(): void + { + $this + ->shouldThrow(\InvalidArgumentException::class) + ->during('__invoke', [new \stdClass()]) + ; + } + + function it_returns_shipping_method_configuration_validation_groups( + ShippingMethodInterface $shippingMethod, + ): void { + $shippingMethod->getCalculator()->willReturn('flat_rate'); + + $this($shippingMethod)->shouldReturn(['rate', 'sylius']); + } + + function it_returns_default_validation_groups( + ShippingMethodInterface $shippingMethod, + ): void { + $shippingMethod->getCalculator()->willReturn(null); + + $this($shippingMethod)->shouldReturn(['sylius']); + } + + function it_returns_gateway_config_validation_groups_if_it_is_shipping_method_form( + FormInterface $form, + ShippingMethodInterface $shippingMethod, + ): void { + $form->getData()->willReturn($shippingMethod); + $shippingMethod->getCalculator()->willReturn('per_unit_rate'); + + $this($form)->shouldReturn(['rate', 'sylius']); + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodCalculatorExistsValidatorSpec.php b/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodCalculatorExistsValidatorSpec.php new file mode 100644 index 0000000000..9b97189556 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodCalculatorExistsValidatorSpec.php @@ -0,0 +1,67 @@ +beConstructedWith( + [ + 'flat_rate' => 'sylius.form.shipping_calculator.flat_rate_configuration.label', + 'per_unit_rate' => 'sylius.form.shipping_calculator.per_unit_rate_configuration.label', + ], + ); + + $this->initialize($executionContext); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_shipping_method_calculator_exists( + Constraint $constraint, + ShippingMethodRuleInterface $shippingMethodRule, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$shippingMethodRule, $constraint]) + ; + } + + function it_adds_violation_to_wrong_shipping_method_calculator( + ExecutionContextInterface $executionContext, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ): void { + $executionContext->buildViolation((new ShippingMethodCalculatorExists())->invalidShippingCalculator)->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->shouldBeCalled()->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + + $this->validate('wrong_calculator', new ShippingMethodCalculatorExists()); + } + + function it_does_not_add_violation_to_correct_shipping_method_calculator(ExecutionContextInterface $executionContext): void + { + $executionContext->buildViolation(Argument::cetera())->shouldNotBeCalled(); + + $this->validate('flat_rate', new ShippingMethodCalculatorExists()); + } +} diff --git a/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodRuleValidatorSpec.php b/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodRuleValidatorSpec.php new file mode 100644 index 0000000000..65c3bb6d35 --- /dev/null +++ b/src/Sylius/Bundle/ShippingBundle/spec/Validator/ShippingMethodRuleValidatorSpec.php @@ -0,0 +1,101 @@ +beConstructedWith( + [ + 'total_weight_greater_than_or_equal' => 'sylius.form.shipping_method_rule.total_weight_greater_than_or_equal', + 'order_total_greater_than_or_equal' => 'sylius.form.shipping_method_rule.items_total_greater_than_or_equal', + 'different_rule' => 'sylius.form.shipping_method_rule.different_rule', + ], + [ + 'total_weight_greater_than_or_equal' => ['sylius', 'total_weight'], + 'order_total_greater_than_or_equal' => ['sylius', 'order_total'], + ], + ); + + $this->initialize($executionContext); + } + + function it_throws_an_exception_if_constraint_is_not_an_instance_of_shipping_method_rule( + Constraint $constraint, + ShippingMethodRuleInterface $shippingMethodRule, + ): void { + $this + ->shouldThrow(UnexpectedTypeException::class) + ->during('validate', [$shippingMethodRule, $constraint]) + ; + } + + function it_adds_violation_to_shipping_method_rule_with_wrong_type( + ExecutionContextInterface $executionContext, + ConstraintViolationBuilderInterface $constraintViolationBuilder, + ShippingMethodRuleInterface $shippingMethodRule, + ): void { + $shippingMethodRule->getType()->willReturn('wrong_rule'); + $executionContext->buildViolation((new ShippingMethodRule())->invalidType)->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->setParameter(Argument::cetera())->shouldBeCalled()->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->atPath(Argument::cetera())->shouldBeCalled()->willReturn($constraintViolationBuilder); + $constraintViolationBuilder->addViolation()->shouldBeCalled(); + + $this->validate($shippingMethodRule, new ShippingMethodRule()); + } + + function it_calls_a_validator_with_group( + ExecutionContextInterface $executionContext, + ShippingMethodRuleInterface $shippingMethodRule, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ): void { + $shippingMethodRule->getType()->willReturn('total_weight_greater_than_or_equal'); + $executionContext->getValidator()->willReturn($validator); + $validator->inContext($executionContext)->willReturn($contextualValidator); + + $contextualValidator->validate($shippingMethodRule, null, ['sylius', 'total_weight'])->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($shippingMethodRule, new ShippingMethodRule(['groups' => ['sylius', 'total_weight']])); + } + + function it_calls_validator_with_default_groups_if_none_assigned_to_shipping_method_rule( + ExecutionContextInterface $executionContext, + ShippingMethodRuleInterface $shippingMethodRule, + ValidatorInterface $validator, + ContextualValidatorInterface $contextualValidator, + ): void { + $shippingMethodRule->getType()->willReturn('different_rule'); + + $executionContext->getValidator()->willReturn($validator); + $validator->inContext($executionContext)->willReturn($contextualValidator); + + $contextualValidator->validate($shippingMethodRule, null, ['sylius'])->willReturn($contextualValidator)->shouldBeCalled(); + + $this->validate($shippingMethodRule, new ShippingMethodRule(['groups' => ['sylius']])); + } +} diff --git a/src/Sylius/Bundle/ShopBundle/Calculator/OrderItemsSubtotalCalculator.php b/src/Sylius/Bundle/ShopBundle/Calculator/OrderItemsSubtotalCalculator.php index 20721c4af0..13fb07d3ac 100644 --- a/src/Sylius/Bundle/ShopBundle/Calculator/OrderItemsSubtotalCalculator.php +++ b/src/Sylius/Bundle/ShopBundle/Calculator/OrderItemsSubtotalCalculator.php @@ -13,17 +13,24 @@ declare(strict_types=1); namespace Sylius\Bundle\ShopBundle\Calculator; +use Sylius\Component\Core\Model\Order; use Sylius\Component\Core\Model\OrderInterface; -use Sylius\Component\Core\Model\OrderItemInterface; +trigger_deprecation( + 'sylius/sylius', + '1.13', + 'The "%s" class is deprecated and will be removed in Sylius 2.0. Items subtotal calculations is now available by using %s::getSubtotalItems method.', + OrderItemsSubtotalCalculator::class, + Order::class, +); + +/** + * @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Items subtotal calculations is now available by using {@see Order::getSubtotalItems} method. + */ final class OrderItemsSubtotalCalculator implements OrderItemsSubtotalCalculatorInterface { public function getSubtotal(OrderInterface $order): int { - return array_reduce( - $order->getItems()->toArray(), - static fn (int $subtotal, OrderItemInterface $item): int => $subtotal + $item->getSubtotal(), - 0, - ); + return $order->getItemsSubtotal(); } } diff --git a/src/Sylius/Bundle/ShopBundle/Calculator/OrderItemsSubtotalCalculatorInterface.php b/src/Sylius/Bundle/ShopBundle/Calculator/OrderItemsSubtotalCalculatorInterface.php index 61bbd695d7..e5826448e6 100644 --- a/src/Sylius/Bundle/ShopBundle/Calculator/OrderItemsSubtotalCalculatorInterface.php +++ b/src/Sylius/Bundle/ShopBundle/Calculator/OrderItemsSubtotalCalculatorInterface.php @@ -15,6 +15,17 @@ namespace Sylius\Bundle\ShopBundle\Calculator; use Sylius\Component\Core\Model\OrderInterface; +trigger_deprecation( + 'sylius/shop-bundle', + '1.13', + 'The "%s" interface is deprecated and will be removed in Sylius 2.0, use method "getItemsSubtotal" from "%s" instead.', + OrderItemsSubtotalCalculatorInterface::class, + OrderInterface::class, +); + +/** + * @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Items subtotal calculations is now available by using {@see Order::getSubtotalItems} method. + */ interface OrderItemsSubtotalCalculatorInterface { public function getSubtotal(OrderInterface $order): int; diff --git a/src/Sylius/Bundle/ShopBundle/Controller/ContactController.php b/src/Sylius/Bundle/ShopBundle/Controller/ContactController.php index 42a768e381..9d409ab1f7 100644 --- a/src/Sylius/Bundle/ShopBundle/Controller/ContactController.php +++ b/src/Sylius/Bundle/ShopBundle/Controller/ContactController.php @@ -14,7 +14,8 @@ declare(strict_types=1); namespace Sylius\Bundle\ShopBundle\Controller; use Sylius\Bundle\CoreBundle\Form\Type\ContactType; -use Sylius\Bundle\ShopBundle\EmailManager\ContactEmailManagerInterface; +use Sylius\Bundle\CoreBundle\Mailer\ContactEmailManagerInterface; +use Sylius\Bundle\ShopBundle\EmailManager\ContactEmailManagerInterface as DeprecatedContactEmailManagerInterface; use Sylius\Component\Channel\Context\ChannelContextInterface; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Customer\Context\CustomerContextInterface; @@ -37,8 +38,18 @@ final class ContactController private ChannelContextInterface $channelContext, private CustomerContextInterface $customerContext, private LocaleContextInterface $localeContext, - private ContactEmailManagerInterface $contactEmailManager, + private ContactEmailManagerInterface|DeprecatedContactEmailManagerInterface $contactEmailManager, ) { + if ($this->contactEmailManager instanceof DeprecatedContactEmailManagerInterface) { + trigger_deprecation( + 'sylius/shop-bundle', + '1.13', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be prohibited in Sylius 2.0. Pass an instance of %s instead.', + DeprecatedContactEmailManagerInterface::class, + self::class, + ContactEmailManagerInterface::class, + ); + } } public function requestAction(Request $request): Response diff --git a/src/Sylius/Bundle/ShopBundle/Controller/RegistrationThankYouController.php b/src/Sylius/Bundle/ShopBundle/Controller/RegistrationThankYouController.php new file mode 100644 index 0000000000..5046393d9a --- /dev/null +++ b/src/Sylius/Bundle/ShopBundle/Controller/RegistrationThankYouController.php @@ -0,0 +1,43 @@ +channelContext->getChannel(); + + if ($channel->isAccountVerificationRequired()) { + return new Response($this->twig->render('@SyliusShop/registerThankYou.html.twig')); + } + + return new RedirectResponse($this->router->generate('sylius_shop_account_dashboard')); + } +} diff --git a/src/Sylius/Bundle/ShopBundle/DependencyInjection/Compiler/BackwardsCompatibility/ReplaceEmailManagersPass.php b/src/Sylius/Bundle/ShopBundle/DependencyInjection/Compiler/BackwardsCompatibility/ReplaceEmailManagersPass.php new file mode 100644 index 0000000000..9b9fdca4cd --- /dev/null +++ b/src/Sylius/Bundle/ShopBundle/DependencyInjection/Compiler/BackwardsCompatibility/ReplaceEmailManagersPass.php @@ -0,0 +1,74 @@ +getDefinitions() as $definition) { + if ($definition->getDecoratedService() === null) { + continue; + } + + $decoratedServiceId = $definition->getDecoratedService()[0]; + + if ($decoratedServiceId === 'sylius.email_manager.contact') { + $this->replaceArgument( + $container, + 'sylius.controller.shop.contact', + ContactController::class, + 6, + 'sylius.email_manager.contact', + ); + + continue; + } + + if ($decoratedServiceId === 'sylius.email_manager.order') { + $this->replaceArgument( + $container, + 'sylius.listener.order_complete', + OrderCompleteListener::class, + 0, + 'sylius.email_manager.order', + ); + } + } + } + + public function replaceArgument( + ContainerBuilder $container, + string $serviceId, + string $serviceClass, + int $argumentIndex, + string $argumentId, + ): void { + if (!$container->hasDefinition($serviceId)) { + return; + } + + $definition = $container->findDefinition($serviceId); + if ($definition->getClass() === $serviceClass) { + $definition->setArgument($argumentIndex, new Reference($argumentId)); + } + } +} diff --git a/src/Sylius/Bundle/ShopBundle/DependencyInjection/SyliusShopExtension.php b/src/Sylius/Bundle/ShopBundle/DependencyInjection/SyliusShopExtension.php index a7a98dd3ae..7d779d2cd3 100644 --- a/src/Sylius/Bundle/ShopBundle/DependencyInjection/SyliusShopExtension.php +++ b/src/Sylius/Bundle/ShopBundle/DependencyInjection/SyliusShopExtension.php @@ -56,7 +56,7 @@ final class SyliusShopExtension extends Extension new Reference('sylius.context.cart'), new Reference('sylius.router.checkout_state'), new Definition(RequestMatcher::class, [$config['pattern']]), - new Reference('sm.factory'), + new Reference('sylius_abstraction.state_machine'), ], ); $checkoutResolverDefinition->addTag('kernel.event_subscriber'); diff --git a/src/Sylius/Bundle/ShopBundle/EmailManager/ContactEmailManager.php b/src/Sylius/Bundle/ShopBundle/EmailManager/ContactEmailManager.php index fe739276a6..90b49edb76 100644 --- a/src/Sylius/Bundle/ShopBundle/EmailManager/ContactEmailManager.php +++ b/src/Sylius/Bundle/ShopBundle/EmailManager/ContactEmailManager.php @@ -17,6 +17,15 @@ use Sylius\Bundle\CoreBundle\Mailer\Emails; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Mailer\Sender\SenderInterface; +trigger_deprecation( + 'sylius/shop-bundle', + '1.13', + 'The "%s" class is deprecated, use "%s" instead.', + ContactEmailManager::class, + \Sylius\Bundle\CoreBundle\Mailer\ContactEmailManager::class, +); + +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see \Sylius\Bundle\CoreBundle\Mailer\ContactEmailManager} instead. */ final class ContactEmailManager implements ContactEmailManagerInterface { public function __construct(private SenderInterface $emailSender) @@ -30,15 +39,21 @@ final class ContactEmailManager implements ContactEmailManagerInterface ?string $localeCode = null, ): void { if ($channel === null) { - @trigger_error( - sprintf('Not passing channel into %s::%s is deprecated since Sylius 1.7', __CLASS__, __METHOD__), - \E_USER_DEPRECATED, + trigger_deprecation( + 'sylius/shop-bundle', + '1.7', + 'Not passing a $channel into %s::%s is deprecated.', + __CLASS__, + __METHOD__, ); } if ($localeCode === null) { - @trigger_error( - sprintf('Not passing locale code into %s::%s is deprecated since Sylius 1.7', __CLASS__, __METHOD__), - \E_USER_DEPRECATED, + trigger_deprecation( + 'sylius/shop-bundle', + '1.7', + 'Not passing a $localeCode into %s::%s is deprecated.', + __CLASS__, + __METHOD__, ); } diff --git a/src/Sylius/Bundle/ShopBundle/EmailManager/ContactEmailManagerInterface.php b/src/Sylius/Bundle/ShopBundle/EmailManager/ContactEmailManagerInterface.php index a438949edd..1ec99ab296 100644 --- a/src/Sylius/Bundle/ShopBundle/EmailManager/ContactEmailManagerInterface.php +++ b/src/Sylius/Bundle/ShopBundle/EmailManager/ContactEmailManagerInterface.php @@ -15,6 +15,15 @@ namespace Sylius\Bundle\ShopBundle\EmailManager; use Sylius\Component\Core\Model\ChannelInterface; +trigger_deprecation( + 'sylius/shop-bundle', + '1.13', + 'The "%s" interface is deprecated, use "%s" instead.', + ContactEmailManagerInterface::class, + \Sylius\Bundle\CoreBundle\Mailer\ContactEmailManagerInterface::class, +); + +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see \Sylius\Bundle\CoreBundle\Mailer\ContactEmailManagerInterface} instead. */ interface ContactEmailManagerInterface { public function sendContactRequest( diff --git a/src/Sylius/Bundle/ShopBundle/EmailManager/OrderEmailManager.php b/src/Sylius/Bundle/ShopBundle/EmailManager/OrderEmailManager.php index 29b11fc2bc..abda4a2ddc 100644 --- a/src/Sylius/Bundle/ShopBundle/EmailManager/OrderEmailManager.php +++ b/src/Sylius/Bundle/ShopBundle/EmailManager/OrderEmailManager.php @@ -19,22 +19,21 @@ use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Mailer\Sender\SenderInterface; use Webmozart\Assert\Assert; +trigger_deprecation( + 'sylius/shop-bundle', + '1.13', + 'The "%s" class is deprecated, use "%s" instead.', + OrderEmailManager::class, + \Sylius\Bundle\CoreBundle\Mailer\OrderEmailManager::class, +); + +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see \Sylius\Bundle\CoreBundle\Mailer\OrderEmailManager} instead. */ final class OrderEmailManager implements OrderEmailManagerInterface { public function __construct( private SenderInterface $emailSender, private ?DecoratedOrderEmailManagerInterface $decoratedEmailManager, ) { - if ($decoratedEmailManager === null) { - @trigger_error( - sprintf( - 'Not passing an instance of %s to %s constructor is deprecated since Sylius 1.8 and will be removed in Sylius 2.0.', - DecoratedOrderEmailManagerInterface::class, - self::class, - ), - \E_USER_DEPRECATED, - ); - } } public function sendConfirmationEmail(OrderInterface $order): void diff --git a/src/Sylius/Bundle/ShopBundle/EmailManager/OrderEmailManagerInterface.php b/src/Sylius/Bundle/ShopBundle/EmailManager/OrderEmailManagerInterface.php index eb5b4b777e..b71882cc44 100644 --- a/src/Sylius/Bundle/ShopBundle/EmailManager/OrderEmailManagerInterface.php +++ b/src/Sylius/Bundle/ShopBundle/EmailManager/OrderEmailManagerInterface.php @@ -15,6 +15,15 @@ namespace Sylius\Bundle\ShopBundle\EmailManager; use Sylius\Component\Core\Model\OrderInterface; +trigger_deprecation( + 'sylius/shop-bundle', + '1.13', + 'The "%s" interface is deprecated, use "%s" instead.', + OrderEmailManagerInterface::class, + \Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface::class, +); + +/** @deprecated since Sylius 1.13 and will be removed in Sylius 2.0. Use {@see \Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface} instead. */ interface OrderEmailManagerInterface { public function sendConfirmationEmail(OrderInterface $order): void; diff --git a/src/Sylius/Bundle/ShopBundle/EventListener/CustomerEmailUpdaterListener.php b/src/Sylius/Bundle/ShopBundle/EventListener/CustomerEmailUpdaterListener.php index b7de5ddfd9..9a6b2c2e18 100644 --- a/src/Sylius/Bundle/ShopBundle/EventListener/CustomerEmailUpdaterListener.php +++ b/src/Sylius/Bundle/ShopBundle/EventListener/CustomerEmailUpdaterListener.php @@ -40,7 +40,14 @@ final class CustomerEmailUpdaterListener private TokenStorageInterface $tokenStorage, ) { if ($requestStackOrSession instanceof SessionInterface) { - trigger_deprecation('sylius/shop-bundle', '1.12', sprintf('Passing an instance of %s as constructor argument for %s is deprecated as of Sylius 1.12 and will be removed in 2.0. Pass an instance of %s instead.', SessionInterface::class, self::class, RequestStack::class)); + trigger_deprecation( + 'sylius/shop-bundle', + '1.12', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be removed in Sylius 2.0. Pass an instance of %s instead.', + SessionInterface::class, + self::class, + RequestStack::class, + ); } } diff --git a/src/Sylius/Bundle/ShopBundle/EventListener/OrderCompleteListener.php b/src/Sylius/Bundle/ShopBundle/EventListener/OrderCompleteListener.php index ab3e0e40e9..73189d433a 100644 --- a/src/Sylius/Bundle/ShopBundle/EventListener/OrderCompleteListener.php +++ b/src/Sylius/Bundle/ShopBundle/EventListener/OrderCompleteListener.php @@ -13,15 +13,26 @@ declare(strict_types=1); namespace Sylius\Bundle\ShopBundle\EventListener; -use Sylius\Bundle\ShopBundle\EmailManager\OrderEmailManagerInterface; +use Sylius\Bundle\CoreBundle\Mailer\OrderEmailManagerInterface; +use Sylius\Bundle\ShopBundle\EmailManager\OrderEmailManagerInterface as DeprecatedOrderEmailManagerInterface; use Sylius\Component\Core\Model\OrderInterface; use Symfony\Component\EventDispatcher\GenericEvent; use Webmozart\Assert\Assert; final class OrderCompleteListener { - public function __construct(private OrderEmailManagerInterface $orderEmailManager) + public function __construct(private DeprecatedOrderEmailManagerInterface|OrderEmailManagerInterface $orderEmailManager) { + if ($this->orderEmailManager instanceof DeprecatedOrderEmailManagerInterface) { + trigger_deprecation( + 'sylius/shop-bundle', + '1.13', + 'Passing an instance of %s as constructor argument for %s is deprecated and will be prohibited in Sylius 2.0. Pass an instance of %s instead.', + DeprecatedOrderEmailManagerInterface::class, + self::class, + OrderEmailManagerInterface::class, + ); + } } public function sendConfirmationEmail(GenericEvent $event): void diff --git a/src/Sylius/Bundle/ShopBundle/Resources/config/routing/security.yml b/src/Sylius/Bundle/ShopBundle/Resources/config/routing/security.yml index b914ab6c5b..45fcf461b3 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/config/routing/security.yml +++ b/src/Sylius/Bundle/ShopBundle/Resources/config/routing/security.yml @@ -30,7 +30,7 @@ sylius_shop_register: form: Sylius\Bundle\CoreBundle\Form\Type\Customer\CustomerRegistrationType event: register redirect: - route: sylius_shop_account_dashboard + route: sylius_shop_register_thank_you flash: sylius.customer.register sylius_shop_register_after_checkout: @@ -47,9 +47,15 @@ sylius_shop_register_after_checkout: template: "@SyliusShop/register.html.twig" event: register redirect: - route: sylius_shop_account_dashboard + route: sylius_shop_register_thank_you flash: sylius.customer.register +sylius_shop_register_thank_you: + path: /register/thank-you + methods: [GET] + defaults: + _controller: sylius.controller.shop.register_thank_you::thankYouAction + sylius_shop_request_password_reset_token: path: /forgotten-password methods: [GET, POST] diff --git a/src/Sylius/Bundle/ShopBundle/Resources/config/services/controller.xml b/src/Sylius/Bundle/ShopBundle/Resources/config/services/controller.xml index 9d2bc2b3de..b3ec867ac1 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/config/services/controller.xml +++ b/src/Sylius/Bundle/ShopBundle/Resources/config/services/controller.xml @@ -22,7 +22,7 @@ - + @@ -46,5 +46,13 @@ + + + + + + + + diff --git a/src/Sylius/Bundle/ShopBundle/Resources/config/services/email.xml b/src/Sylius/Bundle/ShopBundle/Resources/config/services/email.xml index 523573aab1..6512b0fff6 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/config/services/email.xml +++ b/src/Sylius/Bundle/ShopBundle/Resources/config/services/email.xml @@ -17,13 +17,27 @@ + The "%service_id%" service is deprecated since 1.13 and will be removed in 2.0. Use "sylius.mailer.contact_email_manager.shop" instead. + + + The "%alias_id%" service is deprecated since 1.13 and will be removed in 2.0. Use "sylius.mailer.contact_email_manager.shop" instead. - + The "%service_id%" service is deprecated since 1.13 and will be removed in 2.0. Use "sylius.mailer.order_email_manager.shop" instead. + + + The "%alias_id%" service is deprecated since 1.13 and will be removed in 2.0. Use "sylius.mailer.order_email_manager.shop" instead. + + + + + + + + - diff --git a/src/Sylius/Bundle/ShopBundle/Resources/config/services/listeners.xml b/src/Sylius/Bundle/ShopBundle/Resources/config/services/listeners.xml index 68cfa47545..1c40c457de 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/config/services/listeners.xml +++ b/src/Sylius/Bundle/ShopBundle/Resources/config/services/listeners.xml @@ -45,7 +45,7 @@ - + diff --git a/src/Sylius/Bundle/ShopBundle/Resources/config/services/twig.xml b/src/Sylius/Bundle/ShopBundle/Resources/config/services/twig.xml index cf8c6de378..6fe4b8934d 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/config/services/twig.xml +++ b/src/Sylius/Bundle/ShopBundle/Resources/config/services/twig.xml @@ -17,7 +17,6 @@ - The "%service_id%" is deprecated since Sylius 1.12 and will be removed in 2.0. Use methods "getTaxExcludedTotal" and "getTaxIncludedTotal" from "Sylius\Component\Core\Model\Order" instead. diff --git a/src/Sylius/Bundle/ShopBundle/Resources/private/js/sylius-variants-prices.js b/src/Sylius/Bundle/ShopBundle/Resources/private/js/sylius-variants-prices.js index 0172e6e294..8e59ce7062 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/private/js/sylius-variants-prices.js +++ b/src/Sylius/Bundle/ShopBundle/Resources/private/js/sylius-variants-prices.js @@ -58,6 +58,16 @@ const handleProductOptionsChange = function handleProductOptionsChange() { $('#product-price').text($('#sylius-variants-pricing').attr('data-unavailable-text')); $('button[type=submit]').attr('disabled', 'disabled'); } + + const lowestPriceBeforeDiscount = $('#sylius-variants-pricing').find(selector).attr('data-lowest-price-before-discount'); + + if (lowestPriceBeforeDiscount !== undefined) { + $('#lowest-price-before-discount') + .html(lowestPriceBeforeDiscount) + .css({ 'white-space': 'nowrap', display: 'inline' }); + } else { + $('#lowest-price-before-discount').css('display', 'none'); + } }); }; @@ -79,6 +89,16 @@ const handleProductVariantsChange = function handleProductVariantsChange() { } else { $('#product-original-price').css('display', 'none'); } + + const lowestPriceBeforeDiscount = priceRow.attr('data-lowest-price-before-discount'); + + if (lowestPriceBeforeDiscount !== undefined) { + $('#lowest-price-before-discount') + .html(lowestPriceBeforeDiscount) + .css({ 'white-space': 'nowrap', display: 'inline' }); + } else { + $('#lowest-price-before-discount').css('display', 'none'); + } }); }; diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Cart/Summary/_item.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Cart/Summary/_item.html.twig index 52eb150659..5f767490a7 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Cart/Summary/_item.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Cart/Summary/_item.html.twig @@ -16,7 +16,10 @@ {{ money.convertAndFormat(item.discountedUnitPrice) }} - {{ form_widget(form.quantity, sylius_test_form_attribute('cart-item-quantity-input', item.productName)|sylius_merge_recursive({'attr': {'form': main_form}})) }} + + {{ form_widget(form.quantity, sylius_test_form_attribute('cart-item-quantity-input', item.productName)|sylius_merge_recursive({'attr': {'form': main_form}})) }} + {{ form_errors(form.quantity) }} +
diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Cart/Summary/_totals.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Cart/Summary/_totals.html.twig index c013f20783..0d8ebff738 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Cart/Summary/_totals.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Cart/Summary/_totals.html.twig @@ -1,6 +1,6 @@ {% import "@SyliusShop/Common/Macro/money.html.twig" as money %} -{% set itemsSubtotal = sylius_order_items_subtotal(cart) %} +{% set itemsSubtotal = cart.getItemsSubtotal() %} {% set taxIncluded = cart.getTaxIncludedTotal() %} {% set taxExcluded = cart.getTaxExcludedTotal() %} diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Checkout/_summary.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Checkout/_summary.html.twig index c2d93b96df..f77662bcb9 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Checkout/_summary.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Checkout/_summary.html.twig @@ -1,6 +1,6 @@ {% import "@SyliusShop/Common/Macro/money.html.twig" as money %} -{% set itemsSubtotal = sylius_order_items_subtotal(order) %} +{% set itemsSubtotal = order.getItemsSubtotal() %} {% set taxIncluded = order.getTaxIncludedTotal() %} {% set taxExcluded = order.getTaxExcludedTotal() %} diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Common/Macro/money.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Common/Macro/money.html.twig index 23f22ff01e..2df8e399ed 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Common/Macro/money.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Common/Macro/money.html.twig @@ -19,3 +19,9 @@ {{- convertAndFormat(variant|sylius_calculate_original_price({'channel': sylius.channel})) }} {%- endmacro -%} + +{%- macro calculateLowestPrice(variant) -%} + {% from _self import convertAndFormat %} + + {{- convertAndFormat(variant|sylius_calculate_lowest_price({'channel': sylius.channel})) -}} +{%- endmacro -%} diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Common/Order/Table/_totals.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Common/Order/Table/_totals.html.twig index 55bf836433..fc8a18603d 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Common/Order/Table/_totals.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Common/Order/Table/_totals.html.twig @@ -1,6 +1,6 @@ {% import "@SyliusShop/Common/Macro/money.html.twig" as money %} -{% set itemsSubtotal = sylius_order_items_subtotal(order) %} +{% set itemsSubtotal = order.getItemsSubtotal() %} {% set taxIncluded = order.getTaxIncludedTotal() %} {% set taxExcluded = order.getTaxExcludedTotal() %} diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Email/orderConfirmation.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Email/orderConfirmation.html.twig index 6672666d4a..f2e1b3aeb9 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Email/orderConfirmation.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Email/orderConfirmation.html.twig @@ -1,4 +1,4 @@ -{% deprecated 'The "@SyliusShop/Email/orderConfirmation.html.twig" template is deprecated since Sylius 1.8 and will be removed in Sylius 2.0, use "@SyliusCore/Email/layout.html.twig" instead.' %} +{% deprecated 'The "@SyliusShop/Email/orderConfirmation.html.twig" template is deprecated since Sylius 1.8 and will be removed in Sylius 2.0, use "@SyliusCore/Email/orderConfirmation.html.twig" instead.' %} {% extends '@SyliusShop/Email/layout.html.twig' %} {% block subject %} diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Email/verification.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Email/verification.html.twig index 80bf1056f5..ae2edbb461 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Email/verification.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Email/verification.html.twig @@ -1 +1,23 @@ -{% extends '@SyliusCore/Email/accountVerification.html.twig' %} +{% extends '@SyliusCore/Email/layout.html.twig' %} + +{% block subject %} + {{ 'sylius.email.user.account_verification.subject'|trans({}, null, localeCode)|raw }} +{% endblock %} + +{% block content %} + {% set url = sylius_channel_url(path('sylius_shop_user_verification', { 'token': user.emailVerificationToken, '_locale': localeCode }), channel) %} +
+
+ {{ 'sylius.email.user.account_verification.greeting'|trans({}, null, localeCode) }} + {{ user.username }}
+
+ {{ 'sylius.email.user.account_verification.strategy'|trans({}, null, localeCode) }}: +
+ + +{% endblock %} diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/Tabs/Reviews/_viewMore.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/Tabs/Reviews/_viewMore.html.twig index 74718b70cd..f38dea9b9c 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/Tabs/Reviews/_viewMore.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/Tabs/Reviews/_viewMore.html.twig @@ -1,3 +1,5 @@ - -
{{ 'sylius.ui.view_more'|trans }}
-
+{% if product.acceptedReviews.count > 0 %} + +
{{ 'sylius.ui.view_more'|trans }}
+
+{% endif %} diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_price.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_price.html.twig index 9956911bf8..4bdac253ed 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_price.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_price.html.twig @@ -12,3 +12,18 @@ {{ money.calculatePrice(variant) }} + +{% set days = sylius.channel.channelPriceHistoryConfig.lowestPriceForDiscountedProductsCheckingPeriod %} +{% set hasLowestPrice = variant|sylius_has_lowest_price({'channel': sylius.channel})%} + +
+ {% if hasDiscount and hasLowestPrice %} + {{ 'sylius.ui.lowest_price_days_before_discount_was'|trans({'%days%': days, '%price%': money.calculateLowestPrice(variant)}) }} + {% endif %} +
diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variantSelection.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variantSelection.html.twig index c6fe9c6ec4..89c5b54787 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variantSelection.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variantSelection.html.twig @@ -1,5 +1,5 @@ {% if product.isConfigurable() and product.getVariantSelectionMethod() == 'match' and not product.enabledVariants.empty() %} - {% include '@SyliusShop/Product/Show/_variantsPricing.html.twig' with {'pricing': sylius_product_variant_prices(product, sylius.channel), 'variants': product.enabledVariants} %} + {% include '@SyliusShop/Product/Show/_variantsPricing.html.twig' with {'pricing': sylius_product_variants_map(product, {'channel': sylius.channel}), 'variants': product.enabledVariants} %} {% endif %} {% include '@SyliusShop/Product/Show/_inventory.html.twig' %} diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variants.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variants.html.twig index edb68b8e87..8082d542dc 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variants.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variants.html.twig @@ -25,7 +25,24 @@ {% endif %} {% set appliedPromotions = channelPricing.appliedPromotions|map(promotion => ({'label': promotion.label, 'description': promotion.description})) %} - + {% set days = sylius.channel.channelPriceHistoryConfig.lowestPriceForDiscountedProductsCheckingPeriod %} + {% set hasLowestPrice = variant|sylius_has_lowest_price({'channel': sylius.channel})%} + {% set hasDiscount = variant|sylius_has_discount({'channel': sylius.channel}) %} + + {{ money.calculatePrice(variant) }} diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variantsPricing.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variantsPricing.html.twig index f2e80bda78..45e0bb08f7 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variantsPricing.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Product/Show/_variantsPricing.html.twig @@ -1,13 +1,15 @@ {% import "@SyliusShop/Common/Macro/money.html.twig" as money %} +{% set days = sylius.channel.channelPriceHistoryConfig.lowestPriceForDiscountedProductsCheckingPeriod %} +
-{% for price in pricing %} +{% for option_data in pricing %} {% set catalog_promotions = [] %} - {% if price.applied_promotions is defined %} - {% for promotion in price.applied_promotions %} + {% if option_data.applied_promotions is defined %} + {% for promotion in option_data.applied_promotions %} {% set catalog_promotions = catalog_promotions|merge([{'label': promotion.name, 'description': promotion.description}]) %} {% endfor %} {% endif %} -
+
{% endfor %}
diff --git a/src/Sylius/Bundle/ShopBundle/Resources/views/Taxon/_horizontalMenu.html.twig b/src/Sylius/Bundle/ShopBundle/Resources/views/Taxon/_horizontalMenu.html.twig index ffa7b68fa1..479a9a2a94 100644 --- a/src/Sylius/Bundle/ShopBundle/Resources/views/Taxon/_horizontalMenu.html.twig +++ b/src/Sylius/Bundle/ShopBundle/Resources/views/Taxon/_horizontalMenu.html.twig @@ -2,7 +2,7 @@ {% import _self as macros %} {% if taxon.enabledChildren|length > 0 %} -