diff --git a/.docker/Dockerfile b/.docker/Dockerfile new file mode 100644 index 00000000..f262b23d --- /dev/null +++ b/.docker/Dockerfile @@ -0,0 +1,73 @@ +FROM ubuntu:20.04 +ARG DEBIAN_FRONTEND=noninteractive +ARG PHP_VERSION=8.2 +ENV LC_ALL=C.UTF-8 + +# Install basic tools +RUN apt-get update && apt-get install -y \ + software-properties-common \ + curl \ + make \ + supervisor \ + unzip \ + python2 \ + g++ + +# Append NODE, NGINX and PHP repositories +RUN add-apt-repository ppa:ondrej/php \ + && add-apt-repository ppa:ondrej/nginx \ + && curl -sL https://deb.nodesource.com/setup_14.x | bash - + +# Install required PHP extensions +RUN apt-get update && apt-get install -y \ + nodejs \ + nginx \ + php${PHP_VERSION} \ + php${PHP_VERSION}-apcu \ + php${PHP_VERSION}-calendar \ + php${PHP_VERSION}-common \ + php${PHP_VERSION}-cli \ + php${PHP_VERSION}-ctype \ + php${PHP_VERSION}-curl \ + php${PHP_VERSION}-dom \ + php${PHP_VERSION}-exif \ + php${PHP_VERSION}-fpm \ + php${PHP_VERSION}-gd \ + php${PHP_VERSION}-intl \ + php${PHP_VERSION}-mbstring \ + php${PHP_VERSION}-mysql \ + php${PHP_VERSION}-opcache \ + php${PHP_VERSION}-pdo \ + php${PHP_VERSION}-pgsql \ + php${PHP_VERSION}-sqlite \ + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-xsl \ + php${PHP_VERSION}-yaml \ + php${PHP_VERSION}-zip + +# Install Composer +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename composer + +# Cleanup +RUN apt-get remove --purge -y software-properties-common curl && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* /usr/share/man/* + +# Create directory for php-fpm socket +# Link php-fpm binary file without version +# -p Creates missing intermediate path name directories +RUN ln -s /usr/sbin/php-fpm${PHP_VERSION} /usr/sbin/php-fpm && mkdir -p /run/php + +# Install yarn +RUN npm install -g yarn && npm cache clean --force + +# Initialize config files +COPY .docker/supervisord.conf /etc/supervisor/conf.d/supervisor.conf +COPY .docker/nginx.conf /etc/nginx/nginx.conf +COPY .docker/fpm.conf /etc/php/${PHP_VERSION}/fpm/pool.d/www.conf +COPY .docker/php.ini /etc/php/${PHP_VERSION}/fpm/php.ini +COPY .docker/php.ini /etc/php/${PHP_VERSION}/cli/php.ini + +WORKDIR /app + +EXPOSE 80 + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/.docker/Dockerfile-xdebug b/.docker/Dockerfile-xdebug new file mode 100644 index 00000000..0faea0ef --- /dev/null +++ b/.docker/Dockerfile-xdebug @@ -0,0 +1,74 @@ +FROM ubuntu:20.04 +ARG DEBIAN_FRONTEND=noninteractive +ARG PHP_VERSION=8.1 +ENV LC_ALL=C.UTF-8 + +# Install basic tools +RUN apt-get update && apt-get install -y \ + software-properties-common \ + curl \ + make \ + supervisor \ + unzip \ + python2 \ + g++ + +# Append NODE, NGINX and PHP repositories +RUN add-apt-repository ppa:ondrej/php \ + && add-apt-repository ppa:ondrej/nginx \ + && curl -sL https://deb.nodesource.com/setup_14.x | bash - + +# Install required PHP extensions +RUN apt-get update && apt-get install -y \ + nodejs \ + nginx \ + php${PHP_VERSION} \ + php${PHP_VERSION}-apcu \ + php${PHP_VERSION}-calendar \ + php${PHP_VERSION}-common \ + php${PHP_VERSION}-cli \ + php${PHP_VERSION}-ctype \ + php${PHP_VERSION}-curl \ + php${PHP_VERSION}-dom \ + php${PHP_VERSION}-exif \ + php${PHP_VERSION}-fpm \ + php${PHP_VERSION}-gd \ + php${PHP_VERSION}-intl \ + php${PHP_VERSION}-mbstring \ + php${PHP_VERSION}-mysql \ + php${PHP_VERSION}-opcache \ + php${PHP_VERSION}-pdo \ + php${PHP_VERSION}-pgsql \ + php${PHP_VERSION}-sqlite \ + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-xsl \ + php${PHP_VERSION}-yaml \ + php${PHP_VERSION}-xdebug \ + php${PHP_VERSION}-zip + +# Install Composer +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename composer + +# Cleanup +RUN apt-get remove --purge -y software-properties-common curl && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* /usr/share/man/* + +# Create directory for php-fpm socket +# Link php-fpm binary file without version +# -p Creates missing intermediate path name directories +RUN ln -s /usr/sbin/php-fpm${PHP_VERSION} /usr/sbin/php-fpm && mkdir -p /run/php + +# Install yarn +RUN npm install -g yarn && npm cache clean --force + +# Initialize config files +COPY .docker/supervisord.conf /etc/supervisor/conf.d/supervisor.conf +COPY .docker/nginx.conf /etc/nginx/nginx.conf +COPY .docker/fpm.conf /etc/php/${PHP_VERSION}/fpm/pool.d/www.conf +COPY .docker/php-xdebug.ini /etc/php/${PHP_VERSION}/fpm/php.ini +COPY .docker/php-xdebug.ini /etc/php/${PHP_VERSION}/cli/php.ini + +WORKDIR /app + +EXPOSE 80 + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/.docker/fpm.conf b/.docker/fpm.conf new file mode 100644 index 00000000..4f0c372e --- /dev/null +++ b/.docker/fpm.conf @@ -0,0 +1,21 @@ +[www] +user = www-data +group = www-data + +listen = /var/run/php-www.sock +listen.owner = www-data +listen.group = www-data +listen.mode = 0660 + +clear_env = no + +pm = dynamic +pm.max_children = 5 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 + +pm.status_path = /status +catch_workers_output = yes + +security.limit_extensions = .php diff --git a/.docker/nginx.conf b/.docker/nginx.conf new file mode 100644 index 00000000..7fa27004 --- /dev/null +++ b/.docker/nginx.conf @@ -0,0 +1,48 @@ +user www-data; +worker_processes auto; +daemon off; +pid /run/nginx.pid; + +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server_tokens off; + + client_max_body_size 64m; + sendfile on; + tcp_nodelay on; + tcp_nopush on; + + gzip_vary on; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + server { + listen 80; + + root /app/tests/Application/public; + index index.php; + + location / { + try_files $uri /index.php$is_args$args; + } + + location ~ \.php$ { + include fastcgi_params; + + fastcgi_pass unix:/var/run/php-www.sock; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + } + } +} diff --git a/.docker/php-xdebug.ini b/.docker/php-xdebug.ini new file mode 100644 index 00000000..717a6bc2 --- /dev/null +++ b/.docker/php-xdebug.ini @@ -0,0 +1,22 @@ +[PHP] +memory_limit=512M + +[date] +date.timezone=${PHP_DATE_TIMEZONE} + +[opcache] +opcache.enable=0 +opcache.memory_consumption=256 +opcache.max_accelerated_files=20000 +opcache.validate_timestamps=0 +;opcache.preload=/app/config/preload.php +opcache.preload_user=www-data +opcache.jit=1255 +opcache.jit_buffer_size=256M + +[xdebug] +xdebug.mode=debug +xdebug.client_host=host.docker.internal +xdebug.start_with_request = yes +xdebug.client_port = 9003 +xdebug.discover_client_host = 1 diff --git a/.docker/php.ini b/.docker/php.ini new file mode 100644 index 00000000..c283a534 --- /dev/null +++ b/.docker/php.ini @@ -0,0 +1,15 @@ +[PHP] +memory_limit=512M + +[date] +date.timezone=${PHP_DATE_TIMEZONE} + +[opcache] +opcache.enable=0 +opcache.memory_consumption=256 +opcache.max_accelerated_files=20000 +opcache.validate_timestamps=0 +;opcache.preload=/app/config/preload.php +opcache.preload_user=www-data +opcache.jit=1255 +opcache.jit_buffer_size=256M diff --git a/.docker/supervisord.conf b/.docker/supervisord.conf new file mode 100644 index 00000000..913adb67 --- /dev/null +++ b/.docker/supervisord.conf @@ -0,0 +1,14 @@ +[supervisord] +nodaemon = true +user = root +pidfile = /run/supervisord.pid + +[program:nginx] +command = /usr/sbin/nginx +user = root +autostart = true + +[program:php-fpm] +command = /usr/sbin/php-fpm -F +user = root +autostart = true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 38ade654..2501b39b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,10 +6,9 @@ on: - 'dependabot/**' pull_request: ~ release: - types: [created] + types: [ created ] schedule: - - - cron: "0 1 * * 6" # Run at 1am every Saturday + - cron: "0 1 * * 6" # Run at 1am every Saturday workflow_dispatch: ~ jobs: @@ -21,29 +20,20 @@ jobs: strategy: fail-fast: false matrix: - php: [ "8.0", "8.1", "8.2", "8.3" ] + php: [ "8.1", "8.2", "8.3" ] symfony: [ "^5.4", "^6.4" ] - sylius: [ "^1.12", "^1.13" ] + sylius: [ "~1.12.17", "~1.13.2" ] node: [ "18.x", "20.x" ] mysql: [ "8.0" ] - exclude: - - sylius: ^1.13 - php: 8.0 - - sylius: ^1.12 - php: 8.0 - symfony: ^6.4 - env: APP_ENV: test DATABASE_URL: "mysql://root:root@127.0.0.1/sylius?serverVersion=${{ matrix.mysql }}" steps: - - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - - name: Setup PHP + - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: "${{ matrix.php }}" @@ -51,130 +41,32 @@ jobs: tools: symfony coverage: none - - - name: Setup Node - uses: actions/setup-node@v4 + - name: Build test application + uses: SyliusLabs/BuildTestAppAction@v1.3 with: + sylius-version: "${{ matrix.sylius }}" + symfony-version: "${{ matrix.symfony }}" + mysql-version: "${{ matrix.mysql }}" node-version: "${{ matrix.node }}" + working-directory: "." + plugin-build: "yes" - - - name: Shutdown default MySQL - run: sudo service mysql stop - - - - name: Setup MySQL - uses: mirromutth/mysql-action@v1.1 - with: - mysql version: "${{ matrix.mysql }}" - mysql root password: "root" - - - - name: Output PHP version for Symfony CLI - run: php -v | head -n 1 | awk '{ print $2 }' > .php-version - - - - name: Install certificates - run: symfony server:ca:install - - - - name: Run Chrome Headless - run: google-chrome-stable --enable-automation --disable-background-networking --no-default-browser-check --no-first-run --disable-popup-blocking --disable-default-apps --allow-insecure-localhost --disable-translate --disable-extensions --no-sandbox --enable-features=Metal --headless --remote-debugging-port=9222 --window-size=2880,1800 --proxy-server='direct://' --proxy-bypass-list='*' http://127.0.0.1 > /dev/null 2>&1 & - - - - name: Run webserver - run: (cd tests/Application && symfony server:start --port=8080 --dir=public --daemon) - - - - name: Get Composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - - name: Cache Composer - uses: actions/cache@v4 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-php-${{ matrix.php }}-composer-${{ hashFiles('**/composer.json **/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php-${{ matrix.php }}-composer- - - - name: Restrict Symfony version - if: matrix.symfony != '' - run: | - composer global config --no-plugins allow-plugins.symfony/flex true - composer global require --no-progress --no-scripts --no-plugins "symfony/flex:^1.10" - composer config extra.symfony.require "${{ matrix.symfony }}" - - - name: Restrict Sylius version - if: matrix.sylius != '' - run: composer require "sylius/sylius:${{ matrix.sylius }}" --no-update --no-scripts --no-interaction - - - - name: Install PHP dependencies - run: composer install --no-interaction - env: - SYMFONY_REQUIRE: ${{ matrix.symfony }} - - - name: Install Behat driver - run: vendor/bin/bdi browser:google-chrome drivers - - - - name: Get Yarn cache directory - id: yarn-cache - run: echo "::set-output name=dir::$(yarn cache dir)" - - - - name: Cache Yarn - uses: actions/cache@v4 - with: - path: ${{ steps.yarn-cache.outputs.dir }} - key: ${{ runner.os }}-node-${{ matrix.node }}-yarn-${{ hashFiles('**/package.json **/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-node-${{ matrix.node }}-yarn- - - - name: Install JS dependencies - run: (cd tests/Application && yarn install) - - - - name: Prepare test application database - run: | - (cd tests/Application && bin/console doctrine:database:create -vvv) - (cd tests/Application && bin/console doctrine:schema:create -vvv) - - - name: Prepare test application assets - run: | - (cd tests/Application && bin/console assets:install public -vvv) - (cd tests/Application && yarn encore dev) - - - name: Prepare test application cache - run: (cd tests/Application && bin/console cache:warmup -vvv) - - - - name: Load fixtures in test application - run: (cd tests/Application && bin/console sylius:fixtures:load -n) - - - - name: Validate composer.json + - name: Validate composer.json run: composer validate --ansi --strict - - - name: Validate database schema + - name: Validate database schema run: (cd tests/Application && bin/console doctrine:schema:validate) - - - name: Run PHPSpec - run: vendor/bin/phpspec run --ansi -f progress --no-interaction + - name: Run API tests + run: vendor/bin/phpunit tests/Api --colors=always - - - name: Run PHPUnit - run: vendor/bin/phpunit --colors=always + - name: Run PHPUnit tests + run: vendor/bin/phpunit tests/Unit --colors=always - - - name: Run Behat + - name: Run Behat run: vendor/bin/behat --colors --strict -vvv --no-interaction || vendor/bin/behat --colors --strict -vvv --no-interaction --rerun - - - name: Upload Behat logs + - name: Upload Behat logs uses: actions/upload-artifact@v3 if: failure() with: @@ -182,8 +74,7 @@ jobs: path: etc/build/ if-no-files-found: ignore - - - name: Failed build Slack notification + - name: Failed build Slack notification uses: rtCamp/action-slack-notify@v2 if: ${{ failure() && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') }} env: diff --git a/.github/workflows/coding_standard.yml b/.github/workflows/coding_standard.yml index 02942c52..4a84c646 100644 --- a/.github/workflows/coding_standard.yml +++ b/.github/workflows/coding_standard.yml @@ -18,18 +18,11 @@ jobs: strategy: fail-fast: false matrix: - php: [ "8.0", "8.1", "8.2", "8.3" ] + php: [ "8.1", "8.2", "8.3" ] symfony: [ "^5.4", "^6.4" ] - sylius: [ "^1.12", "^1.13" ] + sylius: [ "~1.12.17", "~1.13.2" ] node: [ "18.x", "20.x" ] - exclude: - - sylius: ^1.13 - php: 8.0 - - sylius: ^1.12 - php: 8.0 - symfony: ^6.4 - steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index 993371ac..efd112e4 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ This **open-source plugin was developed to help the Sylius community**. If you h composer require bitbag/product-bundle-plugin --no-scripts ``` -2. Add plugin dependencies to your `config/bundles.php` file after `Sylius\Bundle\ApiBundle\SyliusApiBundle`. +2. (optional) Add plugin dependencies to your `config/bundles.php` file after `Sylius\Bundle\ApiBundle\SyliusApiBundle`. ```php return [ @@ -59,18 +59,18 @@ This **open-source plugin was developed to help the Sylius community**. If you h ]; ``` -3. Import required config in your `config/packages/_sylius.yaml` file: +3. (optional) Import required config in your `config/packages/_sylius.yaml` file: ```yaml # config/packages/_sylius.yaml imports: - ... + ... - - { resource: "@BitBagSyliusProductBundlePlugin/Resources/config/config.yml" } + - { resource: "@BitBagSyliusProductBundlePlugin/Resources/config/config.yml" } ``` -4. Import routing in your `config/routes.yaml` file: +4. (optional) Import routing in your `config/routes.yaml` file: ```yaml @@ -78,15 +78,20 @@ This **open-source plugin was developed to help the Sylius community**. If you h ... bitbag_sylius_product_bundle_plugin: - resource: "@BitBagSyliusProductBundlePlugin/Resources/config/routing.yml" + resource: "@BitBagSyliusProductBundlePlugin/Resources/config/routing.yml" ``` -5. Extend `Product`(including Doctrine mapping): +5. (applied if using Rector) Extend entities by running + ```bash + vendor/bin/rector process src --config=vendor/bitbag/product-bundle-plugin/rector.php + ``` + +6. (applied if not using Rector) Extend `Product` (including Doctrine mapping): ```php - - - - + + + + ``` -7. Extend `OrderItem` (including Doctrine mapping): +8. (applied if not using Rector) Extend `OrderItem` (including Doctrine mapping): ```php ``` -9. Add configuration for extended product, order item and product variant repository: +10. (optional) If you want to manage stock for bundled products instead of managing bundle stock, add this to your .env: + + ```dotenv + ###> bitbag/product-bundle-plugin ### + BUNDLED_PRODUCTS_INVENTORY_MANAGEMENT_FEATURE=true + ###< bitbag/product-bundle-plugin ### + ``` + +11. Add configuration for extended product and order item: ```yaml # config/packages/_sylius.yaml sylius_product: - resources: - product: - classes: - model: App\Entity\Product\Product - product_variant: - classes: - repository: BitBag\SyliusProductBundlePlugin\Repository\ProductVariantRepository - sylius_order: - resources: - order_item: - classes: - model: App\Entity\Order\OrderItem + resources: + product: + classes: + model: App\Entity\Product\Product + + sylius_order: + resources: + order_item: + classes: + model: App\Entity\Order\OrderItem ``` -10. Add 'Create/Bundle' to product grid configuration: +12. If you have full configuration in xml override doctrine config: + +```yaml +# config/packages/doctrine.yaml +doctrine: + orm: + entity_managers: + default: + mappings: + App: + is_bundle: false + type: xml + dir: '%kernel.project_dir%/src/Resources/config/doctrine' + prefix: 'App\Entity' + alias: App - ```yaml - # config/packages/_sylius.yaml - - sylius_grid: - grids: - sylius_admin_product: - actions: - main: - create: - type: links - label: sylius.ui.create - options: - class: primary - icon: plus - header: - icon: cube - label: sylius.ui.type - links: - simple: - label: sylius.ui.simple_product - icon: plus - route: sylius_admin_product_create_simple - configurable: - label: sylius.ui.configurable_product - icon: plus - route: sylius_admin_product_create - bundle: - label: bitbag_sylius_product_bundle.ui.bundle - icon: plus - route: bitbag_product_bundle_admin_product_create_bundle - - ``` -11. If you have full configuration in xml override doctrine config : +``` - ```yaml - # config/packages/doctrine.yaml - - mappings: - App: - is_bundle: false - type: xml - dir: '%kernel.project_dir%/src/Resources/config/doctrine' - prefix: 'App\Entity' - alias: App - - - ``` - -12. Copy plugin templates into your project `templates/bundles` directory: +13. Add plugin templates: +- Inject blocks: + +```yaml +sylius_ui: + events: + sylius.shop.product.show.right_sidebar: + blocks: + variant_selection: + template: "@BitBagSyliusProductBundlePlugin/Shop/Product/_variantSelection.html.twig" + priority: 10 + + sylius.shop.layout.javascripts: + blocks: + plugin_scripts: + template: "@BitBagSyliusProductBundlePlugin/Shop/_scripts.html.twig" + priority: 20 + + sylius.shop.layout.stylesheets: + blocks: + plugin_stylesheets: + template: "@BitBagSyliusProductBundlePlugin/Shop/_styles.html.twig" + priority: 20 + + sylius.admin.layout.javascripts: + blocks: + plugin_scripts: + template: "@BitBagSyliusProductBundlePlugin/Admin/_scripts.html.twig" + priority: 20 + + sylius.admin.layout.stylesheets: + blocks: + plugin_stylesheets: + template: "@BitBagSyliusProductBundlePlugin/Admin/_styles.html.twig" + priority: 20 + +``` +- Copy plugin templates into your project `templates/bundles` directory: ```bash - $ cp -R vendor/bitbag/product-bundle-plugin/tests/Application/templates/bundles/* templates/bundles/ + cp -R vendor/bitbag/product-bundle-plugin/src/Resources/views/Admin/Order templates/bundles/SyliusAdminBundle + cp -R vendor/bitbag/product-bundle-plugin/src/Resources/views/Admin/Product templates/bundles/SyliusAdminBundle + cp -R vendor/bitbag/product-bundle-plugin/src/Resources/views/Shop/Cart templates/bundles/SyliusShopBundle + cp -R vendor/bitbag/product-bundle-plugin/src/Resources/views/Shop/Common templates/bundles/SyliusShopBundle ``` -13. Please clear application cache by running command below: +14. Please clear application cache by running command below: ```bash - $ bin/console cache:clear + bin/console cache:clear ``` -14. Finish the installation by updating the database schema and installing assets: +15. Finish the installation by updating the database schema and installing assets: ```bash - $ bin/console doctrine:migrations:diff - $ bin/console doctrine:migrations:migrate + bin/console doctrine:migrations:migrate ``` -15. Add plugin assets to your project: +16. Add plugin assets to your project: [Import webpack config](./README_webpack-config.md)* ## Testing @@ -316,15 +280,15 @@ This **open-source plugin was developed to help the Sylius community**. If you h ---- ```bash -$ composer install -$ cd tests/Application -$ yarn install -$ yarn build -$ bin/console assets:install public -e test -$ bin/console doctrine:schema:create -e test -$ bin/console server:run 127.0.0.1:8080 -d public -e test -$ open http://localhost:8080 -$ vendor/bin/behat +composer install +cd tests/Application +yarn install +yarn build +bin/console assets:install public -e test +bin/console doctrine:schema:create -e test +bin/console server:run 127.0.0.1:8080 -d public -e test +open http://localhost:8080 +vendor/bin/behat ``` ## Functionalities diff --git a/README_webpack-config.md b/README_webpack-config.md index fdd6326e..62b2eccc 100644 --- a/README_webpack-config.md +++ b/README_webpack-config.md @@ -43,20 +43,4 @@ webpack_encore: product_bundle_admin: '%kernel.project_dir%/public/build/bitbag/productBundle/admin' ``` -4. Add encore functions to your templates - -```twig -{# @SyliusShopBundle/_scripts.html.twig #} -{{ encore_entry_script_tags('bitbag-productBundle-shop', null, 'product_bundle_shop') }} - -{# @SyliusShopBundle/_styles.html.twig #} -{{ encore_entry_link_tags('bitbag-productBundle-shop', null, 'product_bundle_shop') }} - -{# @SyliusAdminBundle/_scripts.html.twig #} -{{ encore_entry_script_tags('bitbag-productBundle-admin', null, 'product_bundle_admin') }} - -{# @SyliusAdminBundle/_styles.html.twig #} -{{ encore_entry_link_tags('bitbag-productBundle-admin', null, 'product_bundle_admin') }} -``` - -5. Run `yarn encore dev` or `yarn encore production` +4. Run `yarn encore dev` or `yarn encore production` diff --git a/UPGRADE-1.3.md b/UPGRADE-1.3.md deleted file mode 100644 index 9fd41411..00000000 --- a/UPGRADE-1.3.md +++ /dev/null @@ -1,28 +0,0 @@ -# UPGRADE FROM `v1.2.X` TO `v1.3.0` - -## Application - -* Run `composer require sylius/sylius:~1.3.0 --no-update` - -* Add the following code in your `behat.yml(.dist)` file: - - ```yaml - default: - extensions: - FriendsOfBehat\SymfonyExtension: - env_file: ~ - ``` - -* Incorporate changes from the following files into plugin's test application: - - * [`tests/Application/package.json`](https://github.com/Sylius/PluginSkeleton/blob/1.3/tests/Application/package.json) ([see diff](https://github.com/Sylius/PluginSkeleton/pull/134/files#diff-726e1353c14df7d91379c0dea6b30eef)) - * [`tests/Application/.babelrc`](https://github.com/Sylius/PluginSkeleton/blob/1.3/tests/Application/.babelrc) ([see diff](https://github.com/Sylius/PluginSkeleton/pull/134/files#diff-a2527d9d8ad55460b2272274762c9386)) - * [`tests/Application/.eslintrc.js`](https://github.com/Sylius/PluginSkeleton/blob/1.3/tests/Application/.eslintrc.js) ([see diff](https://github.com/Sylius/PluginSkeleton/pull/134/files#diff-396c8c412b119deaa7dd84ae28ae04ca)) - -* Update PHP and JS dependencies by running `composer update` and `(cd tests/Application && yarn upgrade)` - -* Clear cache by running `(cd tests/Application && bin/console cache:clear)` - -* Install assets by `(cd tests/Application && bin/console assets:install web)` and `(cd tests/Application && yarn build)` - -* optionally, remove the build for PHP 7.1. in `.travis.yml` diff --git a/UPGRADE-1.4.md b/UPGRADE-1.4.md deleted file mode 100644 index dc84cad6..00000000 --- a/UPGRADE-1.4.md +++ /dev/null @@ -1,86 +0,0 @@ -# UPGRADE FROM `v1.3.X` TO `v1.4.0` - -First step is upgrading Sylius with composer - -- `composer require sylius/sylius:~1.4.0` - -### Test application database - -#### Migrations - -If you provide migrations with your plugin, take a look at following changes: - -* Change base `AbstractMigration` namespace to `Doctrine\Migrations\AbstractMigration` -* Add `: void` return types to both `up` and `down` functions - -#### Schema update - -If you don't use migrations, just run `(cd tests/Application && bin/console doctrine:schema:update --force)` to update the test application's database schema. - -### Dotenv - -* `composer require symfony/dotenv:^4.2 --dev` -* Follow [Symfony dotenv update guide](https://symfony.com/doc/current/configuration/dot-env-changes.html) to incorporate required changes in `.env` files structure. Remember - they should be done on `tests/Application/` level! Optionally, you can take a look at [corresponding PR](https://github.com/Sylius/PluginSkeleton/pull/156/) introducing these changes in **PluginSkeleton** (this PR also includes changes with Behat - see below) - -Don't forget to clear the cache (`tests/Application/bin/console cache:clear`) to be 100% everything is loaded properly. - ---- - -### Behat - -If you're using Behat and want to be up-to-date with our configuration - -* Update required extensions with `composer require friends-of-behat/symfony-extension:^2.0 friends-of-behat/page-object-extension:^0.3 --dev` -* Remove extensions that are not needed yet with `composer remove friends-of-behat/context-service-extension friends-of-behat/cross-container-extension friends-of-behat/service-container-extension --dev` -* Update your `behat.yml` - look at the diff [here](https://github.com/Sylius/Sylius-Standard/pull/322/files#diff-7bde54db60a6e933518d8b61b929edce) -* Add `SymfonyExtensionBundle` to your `tests/Application/config/bundles.php`: - ```php - return [ - //... - FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['test' => true, 'test_cached' => true], - ]; - ``` -* If you use our Travis CI configuration, follow [these changes](https://github.com/Sylius/PluginSkeleton/pull/156/files#diff-354f30a63fb0907d4ad57269548329e3) introduced in `.travis.yml` file -* Create `tests/Application/config/services_test.yaml` file with the following code and add these your own Behat services as well: - ```yaml - imports: - - { resource: "../../../vendor/sylius/sylius/src/Sylius/Behat/Resources/config/services.xml" } - ``` -* Remove all `__symfony__` prefixes in your Behat services -* Remove all `` tags from your Behat services -* Make your Behat services public by default with `` -* Change `contexts_services ` in your suite definitions to `contexts` -* Take a look at [SymfonyExtension UPGRADE guide](https://github.com/FriendsOfBehat/SymfonyExtension/blob/master/UPGRADE-2.0.md) if you have any more problems - -### Phpstan - -* Fix the container XML path parameter in the `phpstan.neon` file as done [here](https://github.com/Sylius/PluginSkeleton/commit/37fa614dbbcf8eb31b89eaf202b4bd4d89a5c7b3) - -# UPGRADE FROM `v1.2.X` TO `v1.4.0` - -Firstly, check out the [PluginSkeleton 1.3 upgrade guide](https://github.com/Sylius/PluginSkeleton/blob/1.4/UPGRADE-1.3.md) to update Sylius version step by step. -To upgrade to Sylius 1.4 follow instructions from [the previous section](https://github.com/Sylius/PluginSkeleton/blob/1.4/UPGRADE-1.4.md#upgrade-from-v13x-to-v140) with following changes: - -### Doctrine migrations - -* Change namespaces of copied migrations to `Sylius\Migrations` - -### Dotenv - -* These changes are not required, but can be done as well, if you've changed application directory structure in `1.2.x` to `1.3` update - -### Behat - -* Add `\FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle()` to your bundles lists in `tests/Application/AppKernel.php` (preferably only in `test` environment) -* Import Sylius Behat services in `tests/Application/config/config_test.yml` and your own Behat services as well: - ```yaml - imports: - - { resource: "../../../../vendor/sylius/sylius/src/Sylius/Behat/Resources/config/services.xml" } - ``` -* Specify test application's kernel path in `behat.yml`: - ```yaml - FriendsOfBehat\SymfonyExtension: - kernel: - class: AppKernel - path: tests/Application/app/AppKernel.php - ``` diff --git a/composer.json b/composer.json index 4efdb997..6e5712a6 100644 --- a/composer.json +++ b/composer.json @@ -5,9 +5,8 @@ "description": "Product bundle for Sylius.", "license": "MIT", "require": { - "php": "^8.0", - "sylius/sylius": "~1.12.0 || ~1.13.0", - "symfony/webpack-encore-bundle": "^1.12" + "php": "^8.1", + "sylius/sylius": "~1.12.17 || ~1.13.2" }, "require-dev": { "ext-json": "*", @@ -33,25 +32,21 @@ "phpstan/phpstan-webmozart-assert": "^1.2.0", "phpunit/phpunit": "^9.5", "sylius-labs/coding-standard": "^4.0", - "symfony/browser-kit": "^5.4 || ^6.0", - "symfony/debug-bundle": "^5.4 || ^6.0", - "symfony/dotenv": "^5.4 || ^6.0", - "symfony/intl": "^5.4 || ^6.0", - "symfony/web-profiler-bundle": "^5.4 || ^6.0", + "symfony/browser-kit": "^5.4 || ^6.4", + "symfony/debug-bundle": "^5.4 || ^6.4", + "symfony/dotenv": "^5.4 || ^6.4", + "symfony/intl": "^5.4 || ^6.4", + "symfony/web-profiler-bundle": "^5.4 || ^6.4", "vimeo/psalm": "^4.12", "composer/xdebug-handler": "^2.0", "friendsofphp/php-cs-fixer": "^3.0", "lchrusciel/api-test-case": "^5.1", "polishsymfonycommunity/symfony-mocker-container": "^1.0", "php-http/message-factory": "^1.1", - "robertfausk/behat-panther-extension": "^1.1" + "robertfausk/behat-panther-extension": "^1.1", + "sylius/sylius-rector": "^2.0" }, "conflict": { - "symfony/symfony": "4.1.8", - "symfony/browser-kit": "4.1.8", - "symfony/dom-crawler": "4.1.8", - "symfony/routing": "4.1.8", - "symfony/doctrine-bridge": "4.4.16", "symfony/framework-bundle": "6.2.8", "behat/mink-selenium2-driver": ">=1.7.0" }, diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..483d2140 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +services: + app: + container_name: app + build: + context: . + dockerfile: .docker/Dockerfile +# dockerfile: .docker/Dockerfile-xdebug + environment: + APP_ENV: "dev" + DATABASE_URL: "mysql://root:mysql@mysql/sylius_%kernel.environment%?charset=utf8mb4" +# DATABASE_URL: "pgsql://root:postgres@postgres/sylius_%kernel.environment%?charset=utf8" # When using postgres + PHP_DATE_TIMEZONE: "Europe/Warsaw" + PHP_IDE_CONFIG: serverName=PHPSTORM + volumes: + - ./:/app:delegated + - ./.docker/php.ini:/etc/php8/php.ini:delegated + - ./.docker/nginx.conf:/etc/nginx/nginx.conf:delegated + ports: + - 80:80 + depends_on: + - mysql + networks: + - sylius + + mysql: + container_name: mysql + image: mysql:8.0 + platform: linux/amd64 + volumes: + - mysql-data:/var/lib/mysql:rw + environment: + MYSQL_ROOT_PASSWORD: mysql + ports: + - ${MYSQL_PORT:-3306}:3306 + networks: + - sylius + +# postgres: +# image: postgres:14-alpine +# environment: +# POSTGRES_USER: root +# POSTGRES_PASSWORD: postgres +# ports: +# - ${POSTGRES_PORT:-5432}:5432 +# networks: +# - sylius + +networks: + sylius: + driver: bridge + +volumes: + mysql-data: diff --git a/features/creating_bundled_product.feature b/features/creating_bundled_product.feature index be965851..edded4ff 100644 --- a/features/creating_bundled_product.feature +++ b/features/creating_bundled_product.feature @@ -1,34 +1,52 @@ @bundled_product Feature: Creating a product in store which is a bundle of other products - I want to be able to add bundled product to cart + As an Administrator + I want to be able to add bundled product to cart - Background: - Given the store operates on a single channel in "United States" - And I am logged in as an administrator - And the store has a product "Jack Daniels Gentleman" priced at "$10.00" - And the store has a product "Johny Walker Black" priced at "$10.00" - And the store has a product "Jim Beam Double Oak" priced at "$10.00" + Background: + Given the store operates on a single channel in "United States" + And I am logged in as an administrator + And the store has a product "Jack Daniels Gentleman" priced at "$10.00" + And the store has a product "Johny Walker Black" priced at "$10.00" + And the store has a product "Jim Beam Double Oak" priced at "$10.00" - @ui @javascript - Scenario: Creating a bundled product - When I want to create a new bundled product - And I specify its code as "WHISKEY_PACK" - And I name it "Whiskey double pack" in "English (United States)" - And I set its slug to "whiskey-double-pack" in "English (United States)" - And I set its price to "$10.00" for "United States" channel - And I set its original price to "$20.00" for "United States" channel - And I add product "Johny Walker Black" and "Jack Daniels Gentleman" to the bundle - And I add it - Then I should be notified that it has been successfully created + @ui @javascript + Scenario: Creating a bundled product + When I want to create a new bundled product + And I specify its code as "WHISKEY_PACK" + And I name it "Whiskey double pack" in "English (United States)" + And I set its slug to "whiskey-double-pack" in "English (United States)" + And I set its price to "$10.00" for "United States" channel + And I set its original price to "$20.00" for "United States" channel + And I add product "Johny Walker Black" and "Jack Daniels Gentleman" to the bundle + And I add it + Then I should be notified that it has been successfully created - @ui @javascript - Scenario: Creating a bundled product with more products - When I want to create a new bundled product - And I specify its code as "WHISKEY_BIG_PACK" - And I name it "Whiskey triple pack" in "English (United States)" - And I set its slug to "whiskey-triple-pack" in "English (United States)" - And I set its price to "$10.00" for "United States" channel - And I set its original price to "$20.00" for "United States" channel - And I add product "Johny Walker Black" and "Jack Daniels Gentleman" and "Jim Beam Double Oak" to the bundle - And I add it - Then I should be notified that it has been successfully created + @ui @javascript + Scenario: Creating a bundled product with more products + When I want to create a new bundled product + And I specify its code as "WHISKEY_BIG_PACK" + And I name it "Whiskey triple pack" in "English (United States)" + And I set its slug to "whiskey-triple-pack" in "English (United States)" + And I set its price to "$10.00" for "United States" channel + And I set its original price to "$20.00" for "United States" channel + And I add product "Johny Walker Black" and "Jack Daniels Gentleman" and "Jim Beam Double Oak" to the bundle + And I add it + Then I should be notified that it has been successfully created + And there should be a "WHISKEY_BIG_PACK" bundle containing "Johny Walker Black" with quantity 1 + And there should be a "WHISKEY_BIG_PACK" bundle containing "Jack Daniels Gentleman" with quantity 1 + And there should be a "WHISKEY_BIG_PACK" bundle containing "Jim Beam Double Oak" with quantity 1 + + @ui @javascript + Scenario: Creating a bundled product with higher quantity + When I want to create a new bundled product + And I specify its code as "WHISKEY_BIG_PACK" + And I name it "Whiskey triple pack" in "English (United States)" + And I set its slug to "whiskey-triple-pack" in "English (United States)" + And I set its price to "$10.00" for "United States" channel + And I set its original price to "$20.00" for "United States" channel + And I add product "Jim Beam Double Oak" with quantity 5 and "Jack Daniels Gentleman" with quantity 2 to the bundle + And I add it + Then I should be notified that it has been successfully created + And there should be a "WHISKEY_BIG_PACK" bundle containing "Jim Beam Double Oak" with quantity 5 + And there should be a "WHISKEY_BIG_PACK" bundle containing "Jack Daniels Gentleman" with quantity 2 diff --git a/features/having_bundled_product_in_store.feature b/features/having_bundled_product_in_store.feature index 1e9031d2..cfe023bb 100644 --- a/features/having_bundled_product_in_store.feature +++ b/features/having_bundled_product_in_store.feature @@ -1,40 +1,95 @@ @bundled_product Feature: Having a product in store which is a bundle of other products - I want to be able to add bundled product to cart - - Background: - Given the store operates on a single channel in "United States" - And I am a logged in customer - And the store ships everywhere for Free - And the store allows paying Offline - - @ui - Scenario: Adding a product bundle to the cart - Given the store has a product "Jack Daniels Gentleman" priced at "$10.00" - And the store has a product "Johny Walker Black" priced at "$10.00" - And the store has bundled product "Whiskey double pack" priced at "$18.00" which contains "Jack Daniels Gentleman" and "Johny Walker Black" - And all store products appear under a main taxonomy - Then I added product "Whiskey double pack" to the cart - And I should be on my cart summary page - And there should be one item in my cart - - @ui - Scenario: Adding a few product bundles to the cart - Given the store has a product "Jim Beam" priced at "$10.00" - And the store has a product "Jim Beam Double Oak" priced at "$10.00" - And the store has bundled product "Jim Beam double pack" priced at "$18.00" which contains "Jim Beam" and "Jim Beam Double Oak" - And all store products appear under a main taxonomy - Then I added product "Jim Beam double pack" to the cart - And I change product "Jim Beam double pack" quantity to 5 in my cart - And I should see "Jim Beam double pack" with quantity 5 in my cart - - @ui - Scenario: Placing an order for a bundled product - Given the store has a product "Jim Beam" priced at "$10.00" - And the store has a product "Jim Beam Double Oak" priced at "$10.00" - And the store has bundled product "Jim Beam double pack" priced at "$18.00" which contains "Jim Beam" and "Jim Beam Double Oak" - Given I have product "Jim Beam double pack" in the cart - And I specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" - And I proceed with "Free" shipping method and "Offline" payment - And I confirm my order - Then I should see the thank you page + As a Customer + I want to be able to order bundled products + + Background: + Given the store operates on a single channel in "United States" + And the store has "VAT" tax rate of 50% for "Coke" within the "US" zone + And I am a logged in customer + And the store ships everywhere for Free + And the store allows paying Offline + And the store has a product "Jim Beam" priced at "$10.00" + And this product has "Jim Beam 1L" variant priced at "$15.00" identified by "JIM_BEAM_1L" + And the store has a product "Jim Beam Double Oak" priced at "$10.00" + And the store has a product "Coca-Cola" priced at "$5.00" + And the store has bundled product "Jim Beam double pack" priced at "$18.00" which contains "Jim Beam" and "Jim Beam Double Oak" + And the store has bundled product "Jim Beam&Coke" priced at "$12.00" which contains "Jim Beam" and "Coca-Cola" + And it belongs to "Coke" tax category + And all store products appear under a main taxonomy + + @ui + Scenario: Adding product bundles to cart + When I added product "Jim Beam double pack" to the cart + And I change product "Jim Beam double pack" quantity to 5 in my cart + Then I should see "Jim Beam double pack" with quantity 5 in my cart + And my cart total should be "$90.00" + + @ui + Scenario: Placing an order for bundled products + Given I have product "Jim Beam double pack" in the cart + And I have product "Jim Beam&Coke" in the cart + And my cart total should be "$30.00" + And I specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" + And I proceed with "Free" shipping method and "Offline" payment + When I confirm my order + Then I should see the thank you page + + @ui + Scenario: Placing an order for bundled products with promotion applied + Given there is a promotion "Holiday promotion" + And this promotion gives "$1.00" off on every product with minimum price at "$15.00" + And I have product "Jim Beam double pack" in the cart + And I have product "Jim Beam&Coke" in the cart + And my cart total should be "$29.00" + And I specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" + And I proceed with "Free" shipping method and "Offline" payment + When I confirm my order + Then I should see the thank you page + + @ui + Scenario: Placing an order for bundled products with tax applied + Given I have product "Jim Beam double pack" in the cart + And I have product "Jim Beam&Coke" in the cart + And I have product "Coca-Cola" in the cart + When I specified the billing address as "Ankh Morpork", "Frost Alley", "90210", "United States" for "Jon Snow" + And I proceed with "Free" shipping method and "Offline" payment + Then my cart total should be "$41.00" + And my cart taxes should be "$6.00" + + @api + Scenario: Adding product bundles to cart with API + When I pick up my cart + And I add bundle "Jim Beam&Coke" with quantity 5 to my cart + And I add bundle "Jim Beam&Coke" with quantity 5 to my cart + Then I should have bundle "Jim Beam&Coke" with quantity 10 in my cart + And I should have product "Jim Beam" in bundled items + And I should have product "Coca-Cola" in bundled items + + @api + Scenario: Adding unpacked product bundles to cart and overwriting variants with API + When I pick up my cart + And I add bundle "Jim Beam&Coke" with quantity 5 to my cart and overwrite "JIM_BEAM" with "JIM_BEAM_1L" + Then I should have bundle "Jim Beam&Coke" with quantity 5 in my cart + And I should have product variant "JIM_BEAM_1L" in bundled items + And I should not have product variant "JIM_BEAM" in bundled items + And I should have product "Coca-Cola" in bundled items + + @api + Scenario: Adding unpacked product bundles to cart and overwriting variants with invalid variant with API + When I pick up my cart + And I add bundle "Jim Beam&Coke" with quantity 5 to my cart and overwrite "COCA_COLA" with "JIM_BEAM_1L" + Then I should have bundle "Jim Beam&Coke" with quantity 5 in my cart + And I should not have product variant "JIM_BEAM_1L" in bundled items + And I should have product variant "JIM_BEAM" in bundled items + And I should have product "Coca-Cola" in bundled items + + @api + Scenario: Adding packed product bundles to cart and overwriting varians with API + Given product bundle "JIM_BEAM&COKE" is packed + When I pick up my cart + And I add bundle "Jim Beam&Coke" with quantity 5 to my cart and overwrite "JIM_BEAM" with "JIM_BEAM_1L" + Then I should have bundle "Jim Beam&Coke" with quantity 5 in my cart + And I should not have product variant "JIM_BEAM_1L" in bundled items + And I should have product variant "JIM_BEAM" in bundled items + And I should have product "Coca-Cola" in bundled items diff --git a/features/viewing_products_from_ordered_bundle.feature b/features/viewing_products_from_ordered_bundle.feature new file mode 100644 index 00000000..44051955 --- /dev/null +++ b/features/viewing_products_from_ordered_bundle.feature @@ -0,0 +1,37 @@ +@bundled_product +Feature: Reviewing products from ordered bundle + As a Customer + I want to be able to see products in the bundle I ordered + + 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 "Jim Beam" priced at "$10.00" + And the store has a product "Coca-Cola" priced at "$5.00" + And the store has bundled product "Jim Beam&Coke" priced at "$12.00" which contains "Jim Beam" and "Coca-Cola" + And all store products appear under a main taxonomy + And I am a logged in customer with name "Bundle Customer" + + @ui @shop + Scenario: Viewing bundled products in cart + Given I have product "Jim Beam&Coke" in the cart + When I see the summary of my cart + Then there should be one item in my cart + And this item should have name "Jim Beam&Coke" + And there should be bundled products listed + And the list should contain "Jim Beam" + And the list should contain "Coca-Cola" + + @ui @shop + Scenario: Viewing bundled products in order history + Given there is a customer "sylius@example.com" that placed an order "#1" later + And the customer bought a single bundle "Jim Beam&Coke" + And I addressed it to "Ankh Morpork", "Frost Alley", "90210" "Los Angeles" in the "United States" + And for the billing address of "Ankh Morpork" in the "Frost Alley", "90210" "Los Angeles", "United States" + And I chose "Free" shipping method with "Offline" payment + When I view the summary of my order "#1" + Then it should have the number "#1" + And there should be bundled products listed in order details + And the list should contain "Jim Beam" in order details + And the list should contain "Coca-Cola" in order details diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..e74b85e4 --- /dev/null +++ b/rector.php @@ -0,0 +1,33 @@ +ruleWithConfiguration(AddInterfaceToClassExtendingTypeRector::class, [ + 'Sylius\Component\Core\Model\OrderItem' => [ + 'BitBag\SyliusProductBundlePlugin\Entity\OrderItemInterface', + ], + 'Sylius\Component\Core\Model\Product' => [ + 'BitBag\SyliusProductBundlePlugin\Entity\ProductInterface', + ], + ]); + $rectorConfig->ruleWithConfiguration(AddTraitToClassExtendingTypeRector::class, [ + 'Sylius\Component\Core\Model\OrderItem' => [ + 'BitBag\SyliusProductBundlePlugin\Entity\ProductBundleOrderItemsAwareTrait', + ], + 'Sylius\Component\Core\Model\Product' => [ + 'BitBag\SyliusProductBundlePlugin\Entity\ProductBundlesAwareTrait', + ], + ]); +}; diff --git a/spec/EventListener/AddProductToProductBundleWhenEditNormalProductEventListenerSpec.php b/spec/EventListener/AddProductToProductBundleWhenEditNormalProductEventListenerSpec.php deleted file mode 100644 index 84df86b7..00000000 --- a/spec/EventListener/AddProductToProductBundleWhenEditNormalProductEventListenerSpec.php +++ /dev/null @@ -1,39 +0,0 @@ -shouldHaveType(AddProductToProductBundleWhenEditNormalProductEventListener::class); - } - - public function it_should_add_product_to_product_bundle_if_not_exist_on_pre_create_and_update_event( - ResourceControllerEvent $resourceControllerEvent, - ProductInterface $product, - ProductBundleInterface $productBundle - ): void { - $resourceControllerEvent->getSubject()->willReturn($product); - - $product->getProductBundle()->shouldBeCalled(); - - $product->getProductBundle()->willReturn($productBundle); - - $this->addProductToProductBundle($resourceControllerEvent); - } -} diff --git a/src/Command/AddProductBundleItemToCartCommand.php b/src/Command/AddProductBundleItemToCartCommand.php index 369ab358..79d1c60f 100644 --- a/src/Command/AddProductBundleItemToCartCommand.php +++ b/src/Command/AddProductBundleItemToCartCommand.php @@ -14,7 +14,7 @@ use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface; use Sylius\Component\Core\Model\ProductVariantInterface; -final class AddProductBundleItemToCartCommand +final class AddProductBundleItemToCartCommand implements AddProductBundleItemToCartCommandInterface { public function __construct( private ProductBundleItemInterface $productBundleItem, diff --git a/src/Command/AddProductBundleItemToCartCommandInterface.php b/src/Command/AddProductBundleItemToCartCommandInterface.php new file mode 100644 index 00000000..9862a3f3 --- /dev/null +++ b/src/Command/AddProductBundleItemToCartCommandInterface.php @@ -0,0 +1,26 @@ + */ + private Collection $productBundleItems; + public function __construct( - private int $orderId, - private string $productCode, - private int $quantity = 1, + private readonly int $orderId, + private readonly string $productCode, + private readonly int $quantity = 1, ) { + $this->productBundleItems = new ArrayCollection(); } public function getOrderId(): int @@ -34,4 +41,16 @@ public function getQuantity(): int { return $this->quantity; } + + /** @return Collection */ + public function getProductBundleItems(): Collection + { + return $this->productBundleItems; + } + + /** @param Collection $productBundleItems */ + public function setProductBundleItems(Collection $productBundleItems): void + { + $this->productBundleItems = $productBundleItems; + } } diff --git a/src/DataTransformer/AddProductBundleToCartDtoDataTransformer.php b/src/DataTransformer/AddProductBundleToCartDtoDataTransformer.php index 323a6b30..7c3fa895 100644 --- a/src/DataTransformer/AddProductBundleToCartDtoDataTransformer.php +++ b/src/DataTransformer/AddProductBundleToCartDtoDataTransformer.php @@ -14,6 +14,7 @@ use ApiPlatform\Core\DataTransformer\DataTransformerInterface; use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleToCartCommand; use BitBag\SyliusProductBundlePlugin\Dto\Api\AddProductBundleToCartDto; +use BitBag\SyliusProductBundlePlugin\Provider\AddProductBundleItemToCartCommandProviderInterface; use Sylius\Component\Order\Model\OrderInterface; use Webmozart\Assert\Assert; @@ -21,6 +22,11 @@ final class AddProductBundleToCartDtoDataTransformer implements DataTransformerI { public const OBJECT_TO_POPULATE = 'object_to_populate'; + public function __construct( + private readonly AddProductBundleItemToCartCommandProviderInterface $addProductBundleItemToCartCommandProvider, + ) { + } + /** * @param AddProductBundleToCartDto|object $object */ @@ -37,8 +43,16 @@ public function transform( $productCode = $object->getProductCode(); $quantity = $object->getQuantity(); + $overwrittenVariants = $object->getOverwrittenVariants(); + $addItemToCartCommands = $this->addProductBundleItemToCartCommandProvider->provide( + $productCode, + $overwrittenVariants, + ); + + $command = new AddProductBundleToCartCommand($cart->getId(), $productCode, $quantity); + $command->setProductBundleItems($addItemToCartCommands); - return new AddProductBundleToCartCommand($cart->getId(), $productCode, $quantity); + return $command; } public function supportsTransformation( diff --git a/src/DependencyInjection/BitBagSyliusProductBundleExtension.php b/src/DependencyInjection/BitBagSyliusProductBundleExtension.php new file mode 100644 index 00000000..0dad1e66 --- /dev/null +++ b/src/DependencyInjection/BitBagSyliusProductBundleExtension.php @@ -0,0 +1,60 @@ +prependDoctrineMigrations($container); + + $config = $this->getCurrentConfiguration($container); + $this->registerResources('bitbag_sylius_product_bundle', 'doctrine/orm', $config['resources'], $container); + } + + protected function getMigrationsNamespace(): string + { + return 'BitBag\SyliusProductBundlePlugin\Migrations'; + } + + protected function getMigrationsDirectory(): string + { + return '@BitBagSyliusProductBundlePlugin/Migrations'; + } + + protected function getNamespacesOfMigrationsExecutedBefore(): array + { + return ['Sylius\Bundle\CoreBundle\Migrations']; + } + + private function getCurrentConfiguration(ContainerBuilder $container): array + { + /** @var ConfigurationInterface $configuration */ + $configuration = $this->getConfiguration([], $container); + + $configs = $container->getExtensionConfig($this->getAlias()); + + return $this->processConfiguration($configuration, $configs); + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php new file mode 100644 index 00000000..16abc3a3 --- /dev/null +++ b/src/DependencyInjection/Configuration.php @@ -0,0 +1,110 @@ +getRootNode(); + + $rootNode + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('driver')->defaultValue(SyliusResourceBundle::DRIVER_DOCTRINE_ORM)->end() + ->end() + ; + + $this->addResourcesSection($rootNode); + + return $treeBuilder; + } + + private function addResourcesSection(ArrayNodeDefinition $node): void + { + $node + ->children() + ->arrayNode('resources') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('product_bundle') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('classes') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('model')->defaultValue(ProductBundle::class)->cannotBeEmpty()->end() + ->scalarNode('interface')->defaultValue(ProductBundleInterface::class)->cannotBeEmpty()->end() + ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() + ->scalarNode('factory')->defaultValue(Factory::class)->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(ProductBundleRepository::class)->cannotBeEmpty()->end() + ->scalarNode('form')->defaultValue(ProductBundleType::class)->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('product_bundle_item') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('classes') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('model')->defaultValue(ProductBundleItem::class)->cannotBeEmpty()->end() + ->scalarNode('interface')->defaultValue(ProductBundleItemInterface::class)->cannotBeEmpty()->end() + ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() + ->scalarNode('factory')->defaultValue(Factory::class)->cannotBeEmpty()->end() + ->scalarNode('repository')->defaultValue(ProductBundleItemRepository::class)->cannotBeEmpty()->end() + ->scalarNode('form')->defaultValue(ProductBundleItemType::class)->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('product_bundle_order_item') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('classes') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('model')->defaultValue(ProductBundleOrderItem::class)->cannotBeEmpty()->end() + ->scalarNode('interface')->defaultValue(ProductBundleOrderItemInterface::class)->cannotBeEmpty()->end() + ->scalarNode('controller')->defaultValue(ResourceController::class)->cannotBeEmpty()->end() + ->scalarNode('factory')->defaultValue(Factory::class)->cannotBeEmpty()->end() + ->scalarNode('repository')->cannotBeEmpty()->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + } +} diff --git a/src/Dto/Api/AddProductBundleToCartDto.php b/src/Dto/Api/AddProductBundleToCartDto.php index 010434d9..20ab2856 100644 --- a/src/Dto/Api/AddProductBundleToCartDto.php +++ b/src/Dto/Api/AddProductBundleToCartDto.php @@ -19,6 +19,7 @@ public function __construct( private string $productCode, private int $quantity = 1, private ?string $orderTokenValue = null, + private array $overwrittenVariants = [], ) { } @@ -41,4 +42,9 @@ public function getQuantity(): int { return $this->quantity; } + + public function getOverwrittenVariants(): array + { + return $this->overwrittenVariants; + } } diff --git a/src/Entity/ProductBundleOrderItemsAwareTrait.php b/src/Entity/ProductBundleOrderItemsAwareTrait.php index c35892ce..7ae8bd27 100644 --- a/src/Entity/ProductBundleOrderItemsAwareTrait.php +++ b/src/Entity/ProductBundleOrderItemsAwareTrait.php @@ -12,10 +12,24 @@ namespace BitBag\SyliusProductBundlePlugin\Entity; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Mapping as ORM; trait ProductBundleOrderItemsAwareTrait { - /** @var ArrayCollection|ProductBundleOrderItemInterface[] */ + /** + * @var ArrayCollection|ProductBundleOrderItemInterface[] + * + * @ORM\OneToMany( + * targetEntity=ProductBundleOrderItemInterface::class, + * mappedBy="orderItem", + * cascade={"all"} + * ) + */ + #[ORM\OneToMany( + targetEntity: ProductBundleOrderItemInterface::class, + mappedBy: 'orderItem', + cascade: ['all'], + )] protected $productBundleOrderItems; protected function init(): void diff --git a/src/Entity/ProductBundlesAwareTrait.php b/src/Entity/ProductBundlesAwareTrait.php index e054f480..5a76aaa1 100644 --- a/src/Entity/ProductBundlesAwareTrait.php +++ b/src/Entity/ProductBundlesAwareTrait.php @@ -11,9 +11,26 @@ namespace BitBag\SyliusProductBundlePlugin\Entity; +use Doctrine\ORM\Mapping as ORM; + trait ProductBundlesAwareTrait { - /** @var ProductBundleInterface */ + /** + * @var ProductBundleInterface + * + * @ORM\OneToOne( + * targetEntity=ProductBundleInterface::class, + * mappedBy="product", + * cascade={"all"}, + * orphanRemoval=true, + * ) + */ + #[ORM\OneToOne( + targetEntity: ProductBundleInterface::class, + mappedBy: 'product', + cascade: ['all'], + orphanRemoval: true, + )] protected $productBundle; public function getProductBundle(): ?ProductBundleInterface diff --git a/src/Factory/AddProductBundleItemToCartCommandFactory.php b/src/Factory/AddProductBundleItemToCartCommandFactory.php index 7c2f8bd1..03682f53 100644 --- a/src/Factory/AddProductBundleItemToCartCommandFactory.php +++ b/src/Factory/AddProductBundleItemToCartCommandFactory.php @@ -12,11 +12,12 @@ namespace BitBag\SyliusProductBundlePlugin\Factory; use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommand; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface; final class AddProductBundleItemToCartCommandFactory implements AddProductBundleItemToCartCommandFactoryInterface { - public function createNew(ProductBundleItemInterface $bundleItem): AddProductBundleItemToCartCommand + public function createNew(ProductBundleItemInterface $bundleItem): AddProductBundleItemToCartCommandInterface { return new AddProductBundleItemToCartCommand($bundleItem); } diff --git a/src/Factory/AddProductBundleItemToCartCommandFactoryInterface.php b/src/Factory/AddProductBundleItemToCartCommandFactoryInterface.php index cc881b78..b36dac4a 100644 --- a/src/Factory/AddProductBundleItemToCartCommandFactoryInterface.php +++ b/src/Factory/AddProductBundleItemToCartCommandFactoryInterface.php @@ -11,10 +11,10 @@ namespace BitBag\SyliusProductBundlePlugin\Factory; -use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommand; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface; interface AddProductBundleItemToCartCommandFactoryInterface { - public function createNew(ProductBundleItemInterface $bundleItem): AddProductBundleItemToCartCommand; + public function createNew(ProductBundleItemInterface $bundleItem): AddProductBundleItemToCartCommandInterface; } diff --git a/src/Factory/AddProductBundleToCartCommandFactory.php b/src/Factory/AddProductBundleToCartCommandFactory.php index 798e0f00..e0df023b 100644 --- a/src/Factory/AddProductBundleToCartCommandFactory.php +++ b/src/Factory/AddProductBundleToCartCommandFactory.php @@ -11,17 +11,24 @@ namespace BitBag\SyliusProductBundlePlugin\Factory; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleToCartCommand; use BitBag\SyliusProductBundlePlugin\Dto\AddProductBundleToCartDtoInterface; +use Doctrine\Common\Collections\Collection; final class AddProductBundleToCartCommandFactory implements AddProductBundleToCartCommandFactoryInterface { + /** @param Collection $productBundleItems */ public function createNew( int $orderId, string $productCode, int $quantity, + Collection $productBundleItems, ): AddProductBundleToCartCommand { - return new AddProductBundleToCartCommand($orderId, $productCode, $quantity); + $command = new AddProductBundleToCartCommand($orderId, $productCode, $quantity); + $command->setProductBundleItems($productBundleItems); + + return $command; } public function createFromDto(AddProductBundleToCartDtoInterface $dto): AddProductBundleToCartCommand @@ -29,7 +36,8 @@ public function createFromDto(AddProductBundleToCartDtoInterface $dto): AddProdu $cartId = $dto->getCart()->getId(); $productCode = $dto->getProduct()->getCode() ?? ''; $quantity = $dto->getCartItem()->getQuantity(); + $productBundleItems = $dto->getProductBundleItems(); - return $this->createNew($cartId, $productCode, $quantity); + return $this->createNew($cartId, $productCode, $quantity, $productBundleItems); } } diff --git a/src/Factory/AddProductBundleToCartCommandFactoryInterface.php b/src/Factory/AddProductBundleToCartCommandFactoryInterface.php index d2fc089c..01150104 100644 --- a/src/Factory/AddProductBundleToCartCommandFactoryInterface.php +++ b/src/Factory/AddProductBundleToCartCommandFactoryInterface.php @@ -11,15 +11,19 @@ namespace BitBag\SyliusProductBundlePlugin\Factory; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleToCartCommand; use BitBag\SyliusProductBundlePlugin\Dto\AddProductBundleToCartDtoInterface; +use Doctrine\Common\Collections\Collection; interface AddProductBundleToCartCommandFactoryInterface { + /** @param Collection $productBundleItems */ public function createNew( int $orderId, string $productCode, int $quantity, + Collection $productBundleItems, ): AddProductBundleToCartCommand; public function createFromDto(AddProductBundleToCartDtoInterface $dto): AddProductBundleToCartCommand; diff --git a/src/Factory/AddProductBundleToCartDtoFactory.php b/src/Factory/AddProductBundleToCartDtoFactory.php index b98af5d6..b30cd4c0 100644 --- a/src/Factory/AddProductBundleToCartDtoFactory.php +++ b/src/Factory/AddProductBundleToCartDtoFactory.php @@ -11,7 +11,7 @@ namespace BitBag\SyliusProductBundlePlugin\Factory; -use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommand; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Dto\AddProductBundleToCartDto; use BitBag\SyliusProductBundlePlugin\Dto\AddProductBundleToCartDtoInterface; use BitBag\SyliusProductBundlePlugin\Entity\OrderItemInterface; @@ -39,7 +39,7 @@ public function createNew( } /** - * @return AddProductBundleItemToCartCommand[] + * @return AddProductBundleItemToCartCommandInterface[] */ private function getProcessedProductBundleItems(ProductBundleInterface $productBundle): array { diff --git a/src/Factory/ProductBundleOrderItemFactory.php b/src/Factory/ProductBundleOrderItemFactory.php index c18a7e60..808220e0 100644 --- a/src/Factory/ProductBundleOrderItemFactory.php +++ b/src/Factory/ProductBundleOrderItemFactory.php @@ -11,6 +11,7 @@ namespace BitBag\SyliusProductBundlePlugin\Factory; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleOrderItemInterface; use Sylius\Component\Resource\Factory\FactoryInterface; @@ -41,4 +42,17 @@ public function createFromProductBundleItem(ProductBundleItemInterface $bundleIt return $productBundleOrderItem; } + + public function createFromAddProductBundleItemToCartCommand( + AddProductBundleItemToCartCommandInterface $addItemToCartCommand, + ): ProductBundleOrderItemInterface { + /** @var ProductBundleOrderItemInterface $productBundleOrderItem */ + $productBundleOrderItem = $this->decoratedFactory->createNew(); + + $productBundleOrderItem->setProductBundleItem($addItemToCartCommand->getProductBundleItem()); + $productBundleOrderItem->setProductVariant($addItemToCartCommand->getProductVariant()); + $productBundleOrderItem->setQuantity($addItemToCartCommand->getQuantity()); + + return $productBundleOrderItem; + } } diff --git a/src/Factory/ProductBundleOrderItemFactoryInterface.php b/src/Factory/ProductBundleOrderItemFactoryInterface.php index 29669331..3c32cb63 100644 --- a/src/Factory/ProductBundleOrderItemFactoryInterface.php +++ b/src/Factory/ProductBundleOrderItemFactoryInterface.php @@ -11,6 +11,7 @@ namespace BitBag\SyliusProductBundlePlugin\Factory; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleOrderItemInterface; use Sylius\Component\Resource\Factory\FactoryInterface; @@ -18,4 +19,8 @@ interface ProductBundleOrderItemFactoryInterface extends FactoryInterface { public function createFromProductBundleItem(ProductBundleItemInterface $bundleItem): ProductBundleOrderItemInterface; + + public function createFromAddProductBundleItemToCartCommand( + AddProductBundleItemToCartCommandInterface $addItemToCartCommand, + ): ProductBundleOrderItemInterface; } diff --git a/src/Fixture/Factory/ProductBundleFixtureFactory.php b/src/Fixture/Factory/ProductBundleFixtureFactory.php new file mode 100644 index 00000000..c15cd0d4 --- /dev/null +++ b/src/Fixture/Factory/ProductBundleFixtureFactory.php @@ -0,0 +1,72 @@ +optionsResolver = new OptionsResolver(); + $this->configureOptions($this->optionsResolver); + } + + private function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setDefault('bundle', '') + ->setAllowedTypes('bundle', 'string') + ->setDefault('items', []) + ->setAllowedTypes('items', 'array') + ->setDefault('is_packed', '') + ->setAllowedTypes('is_packed', 'bool') + ; + } + + public function create(array $options = []): ProductBundleInterface + { + $options = $this->optionsResolver->resolve($options); + + $bundleProduct = $this->productRepository->findOneByCode($options['bundle']); + /** @var ProductBundleInterface $productBundle */ + $productBundle = $this->productBundleFactory->createNew(); + $productBundle->setProduct($bundleProduct); + $productBundle->setIsPackedProduct($options['is_packed']); + + foreach ($options['items'] ?? [] as $item) { + /** @var ProductVariantInterface $productVariant */ + $productVariant = $this->productVariantRepository->findOneBy(['code' => $item]); + /** @var ProductBundleItemInterface $bundleItem */ + $bundleItem = $this->productBundleItemFactory->createNew(); + $bundleItem->setProductVariant($productVariant); + $bundleItem->setQuantity(1); + $bundleItem->setProductBundle($productBundle); + $productBundle->addProductBundleItem($bundleItem); + } + + return $productBundle; + } +} diff --git a/src/Fixture/ProductBundleFixture.php b/src/Fixture/ProductBundleFixture.php new file mode 100644 index 00000000..0e568b45 --- /dev/null +++ b/src/Fixture/ProductBundleFixture.php @@ -0,0 +1,32 @@ +children(); + $resourceNodeChildren->scalarNode('bundle')->end(); + $resourceNodeChildren->arrayNode('items')->scalarPrototype()->end(); + $resourceNodeChildren->booleanNode('is_packed')->end(); + } + + public function getName(): string + { + return 'product_bundle'; + } +} diff --git a/src/Form/Extension/ProductTypeExtension.php b/src/Form/Extension/ProductTypeExtension.php index 4e1122ba..b211678b 100644 --- a/src/Form/Extension/ProductTypeExtension.php +++ b/src/Form/Extension/ProductTypeExtension.php @@ -25,6 +25,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('productBundle', ProductBundleType::class, [ 'label' => false, 'constraints' => [new Valid()], + 'required' => false, ]) ; } diff --git a/src/Form/Type/AddProductBundleItemToCartType.php b/src/Form/Type/AddProductBundleItemToCartType.php index f4e0f926..099da8cf 100644 --- a/src/Form/Type/AddProductBundleItemToCartType.php +++ b/src/Form/Type/AddProductBundleItemToCartType.php @@ -12,6 +12,7 @@ namespace BitBag\SyliusProductBundlePlugin\Form\Type; use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommand; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductInterface; use Sylius\Bundle\ProductBundle\Form\Type\ProductVariantChoiceType; use Sylius\Bundle\ProductBundle\Form\Type\ProductVariantMatchType; @@ -35,7 +36,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void } $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void { - /** @var AddProductBundleItemToCartCommand $data */ + /** @var AddProductBundleItemToCartCommandInterface $data */ $data = $event->getData(); $form = $event->getForm(); diff --git a/src/Grid/Filter/IsBundleFilter.php b/src/Grid/Filter/IsBundleFilter.php new file mode 100644 index 00000000..f06d224f --- /dev/null +++ b/src/Grid/Filter/IsBundleFilter.php @@ -0,0 +1,40 @@ +productBundleRepository->getProductIds(); + + switch ($data) { + case BooleanFilter::TRUE: + $dataSource->restrict($dataSource->getExpressionBuilder()->in('id', $productBundleIds)); + + break; + case BooleanFilter::FALSE: + $dataSource->restrict($dataSource->getExpressionBuilder()->notIn('id', $productBundleIds)); + + break; + } + } +} diff --git a/src/Handler/AddProductBundleToCartHandler.php b/src/Handler/AddProductBundleToCartHandler.php index 4c4b9832..fc88282e 100644 --- a/src/Handler/AddProductBundleToCartHandler.php +++ b/src/Handler/AddProductBundleToCartHandler.php @@ -23,9 +23,9 @@ final class AddProductBundleToCartHandler implements MessageHandlerInterface { public function __construct( - private OrderRepositoryInterface $orderRepository, - private ProductRepositoryInterface $productRepository, - private CartProcessorInterface $cartProcessor, + private readonly OrderRepositoryInterface $orderRepository, + private readonly ProductRepositoryInterface $productRepository, + private readonly CartProcessorInterface $cartProcessor, ) { } @@ -46,7 +46,10 @@ public function __invoke(AddProductBundleToCartCommand $addProductBundleToCartCo $quantity = $addProductBundleToCartCommand->getQuantity(); Assert::greaterThan($quantity, 0); - $this->cartProcessor->process($cart, $productBundle, $quantity); + $items = $addProductBundleToCartCommand->getProductBundleItems(); + Assert::false($items->isEmpty()); + + $this->cartProcessor->process($cart, $productBundle, $quantity, $items); $this->orderRepository->add($cart); } } diff --git a/src/Handler/AddProductBundleToCartHandler/CartProcessor.php b/src/Handler/AddProductBundleToCartHandler/CartProcessor.php index 7c3b8237..faf794d4 100644 --- a/src/Handler/AddProductBundleToCartHandler/CartProcessor.php +++ b/src/Handler/AddProductBundleToCartHandler/CartProcessor.php @@ -14,6 +14,7 @@ use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleInterface; use BitBag\SyliusProductBundlePlugin\Factory\OrderItemFactoryInterface; use BitBag\SyliusProductBundlePlugin\Factory\ProductBundleOrderItemFactoryInterface; +use Doctrine\Common\Collections\Collection; use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Order\Model\OrderInterface; use Sylius\Component\Order\Modifier\OrderItemQuantityModifierInterface; @@ -23,10 +24,10 @@ final class CartProcessor implements CartProcessorInterface { public function __construct( - private OrderItemQuantityModifierInterface $orderItemQuantityModifier, - private ProductBundleOrderItemFactoryInterface $productBundleOrderItemFactory, - private OrderModifierInterface $orderModifier, - private OrderItemFactoryInterface $cartItemFactory, + private readonly OrderItemQuantityModifierInterface $orderItemQuantityModifier, + private readonly ProductBundleOrderItemFactoryInterface $productBundleOrderItemFactory, + private readonly OrderModifierInterface $orderModifier, + private readonly OrderItemFactoryInterface $cartItemFactory, ) { } @@ -34,6 +35,7 @@ public function process( OrderInterface $cart, ProductBundleInterface $productBundle, int $quantity, + Collection $addBundleItemToCartCommands, ): void { Assert::greaterThan($quantity, 0); @@ -47,8 +49,8 @@ public function process( $cartItem = $this->cartItemFactory->createWithVariant($productVariant); $this->orderItemQuantityModifier->modify($cartItem, $quantity); - foreach ($productBundle->getProductBundleItems() as $bundleItem) { - $productBundleOrderItem = $this->productBundleOrderItemFactory->createFromProductBundleItem($bundleItem); + foreach ($addBundleItemToCartCommands as $addBundleItemToCartCommand) { + $productBundleOrderItem = $this->productBundleOrderItemFactory->createFromAddProductBundleItemToCartCommand($addBundleItemToCartCommand); $cartItem->addProductBundleOrderItem($productBundleOrderItem); } diff --git a/src/Handler/AddProductBundleToCartHandler/CartProcessorInterface.php b/src/Handler/AddProductBundleToCartHandler/CartProcessorInterface.php index f7946c08..aacf225b 100644 --- a/src/Handler/AddProductBundleToCartHandler/CartProcessorInterface.php +++ b/src/Handler/AddProductBundleToCartHandler/CartProcessorInterface.php @@ -12,6 +12,7 @@ namespace BitBag\SyliusProductBundlePlugin\Handler\AddProductBundleToCartHandler; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleInterface; +use Doctrine\Common\Collections\Collection; use Sylius\Component\Order\Model\OrderInterface; interface CartProcessorInterface @@ -20,5 +21,6 @@ public function process( OrderInterface $cart, ProductBundleInterface $productBundle, int $quantity, + Collection $addBundleItemToCartCommands, ): void; } diff --git a/src/Inventory/Checker/BundledProductsInventoryManagementFeatureFlagChecker.php b/src/Inventory/Checker/BundledProductsInventoryManagementFeatureFlagChecker.php new file mode 100644 index 00000000..d962d614 --- /dev/null +++ b/src/Inventory/Checker/BundledProductsInventoryManagementFeatureFlagChecker.php @@ -0,0 +1,25 @@ +enabled; + } +} diff --git a/src/Inventory/Checker/FeatureFlagCheckerInterface.php b/src/Inventory/Checker/FeatureFlagCheckerInterface.php new file mode 100644 index 00000000..58a2f6be --- /dev/null +++ b/src/Inventory/Checker/FeatureFlagCheckerInterface.php @@ -0,0 +1,17 @@ +featureFlagChecker->isEnabled()) { + return $this->decorated->isReservedStockSufficient($orderItem); + } + + /** @var ProductInterface $product */ + $product = $orderItem->getProduct(); + if (!$product->isBundle()) { + return $this->decorated->isReservedStockSufficient($orderItem); + } + + return $this->bundleOrderItemAvailabilityChecker->areOrderedBundledProductVariantsAvailable($orderItem); + } +} diff --git a/src/Inventory/Checker/ProductBundleOrderItemAvailabilityChecker.php b/src/Inventory/Checker/ProductBundleOrderItemAvailabilityChecker.php new file mode 100644 index 00000000..ccea2370 --- /dev/null +++ b/src/Inventory/Checker/ProductBundleOrderItemAvailabilityChecker.php @@ -0,0 +1,41 @@ +getProductBundleOrderItems() as $bundleOrderItem) { + $quantity = $orderItem->getQuantity() * (int) $bundleOrderItem->getQuantity(); + /** @var ProductVariantInterface $variant */ + $variant = $bundleOrderItem->getProductVariant(); + if (!$variant->isTracked()) { + continue; + } + + if (0 > (int) $variant->getOnHold() - $quantity || 0 > (int) $variant->getOnHand() - $quantity) { + return false; + } + } + + return true; + } +} diff --git a/src/Inventory/Checker/ProductBundleOrderItemAvailabilityCheckerInterface.php b/src/Inventory/Checker/ProductBundleOrderItemAvailabilityCheckerInterface.php new file mode 100644 index 00000000..48ad4c1e --- /dev/null +++ b/src/Inventory/Checker/ProductBundleOrderItemAvailabilityCheckerInterface.php @@ -0,0 +1,20 @@ +lockOrderProductVariants($order); + + if (!$this->featureFlagChecker->isEnabled()) { + $this->decorated->cancel($order); + } + + if (in_array( + $order->getPaymentState(), + [OrderPaymentStates::STATE_PAID, OrderPaymentStates::STATE_REFUNDED], + true, + )) { + $this->productBundleOrderInventoryOperator->giveBack($order); + + return; + } + + $this->productBundleOrderInventoryOperator->release($order); + } + + public function hold(OrderInterface $order): void + { + $this->lockOrderProductVariants($order); + + if (!$this->featureFlagChecker->isEnabled()) { + $this->decorated->hold($order); + + return; + } + + $this->productBundleOrderInventoryOperator->hold($order); + } + + public function sell(OrderInterface $order): void + { + $this->lockOrderProductVariants($order); + + if (!$this->featureFlagChecker->isEnabled()) { + $this->decorated->sell($order); + + return; + } + + $this->productBundleOrderInventoryOperator->sell($order); + } + + private function lockOrderProductVariants(OrderInterface $order): void + { + /** @var OrderItemInterface $orderItem */ + foreach ($order->getItems() as $orderItem) { + $this->lockOrderItemProductVariants($orderItem); + } + } + + private function lockOrderItemProductVariants(OrderItemInterface $orderItem): void + { + /** @var ProductInterface $product */ + $product = $orderItem->getProduct(); + if ($this->featureFlagChecker->isEnabled() && $product->isBundle()) { + $this->lockBundledOrderItemProductVariants($orderItem); + } else { + $this->lockOrderItemProductVariant($orderItem); + } + } + + private function lockOrderItemProductVariant(OrderItemInterface $orderItem): void + { + $this->lockProductVariant($orderItem->getVariant()); + } + + private function lockBundledOrderItemProductVariants(OrderItemInterface $orderItem): void + { + foreach ($orderItem->getProductBundleOrderItems() as $bundleOrderItem) { + $this->lockProductVariant($bundleOrderItem->getProductVariant()); + } + } + + private function lockProductVariant(?ProductVariantInterface $variant): void + { + if (null === $variant) { + throw new \InvalidArgumentException('Variant cannot be null'); + } + + if (!$variant->isTracked()) { + return; + } + + $this->productVariantManager->lock($variant, LockMode::OPTIMISTIC, $variant->getVersion()); + } +} diff --git a/src/Inventory/Operator/ProductBundleOrderInventoryOperator.php b/src/Inventory/Operator/ProductBundleOrderInventoryOperator.php new file mode 100644 index 00000000..7515a52a --- /dev/null +++ b/src/Inventory/Operator/ProductBundleOrderInventoryOperator.php @@ -0,0 +1,216 @@ +getItems() as $orderItem) { + /** @var ProductInterface $product */ + $product = $orderItem->getProduct(); + if ($product->isBundle()) { + $this->holdBundleOrderItem($orderItem); + } else { + $this->holdRegularOrderItem($orderItem); + } + } + } + + public function sell(OrderInterface $order): void + { + /** @var OrderItemInterface $orderItem */ + foreach ($order->getItems() as $orderItem) { + /** @var ProductInterface $product */ + $product = $orderItem->getProduct(); + if ($product->isBundle()) { + $this->sellBundleOrderItem($orderItem); + } else { + $this->sellRegularOrderItem($orderItem); + } + } + } + + /** @throws \InvalidArgumentException */ + public function release(OrderInterface $order): void + { + /** @var OrderItemInterface $orderItem */ + foreach ($order->getItems() as $orderItem) { + /** @var ProductInterface $product */ + $product = $orderItem->getProduct(); + if ($product->isBundle()) { + $this->releaseBundleOrderItem($orderItem); + } else { + $this->releaseRegularOrderItem($orderItem); + } + } + } + + public function giveBack(OrderInterface $order): void + { + /** @var OrderItemInterface $orderItem */ + foreach ($order->getItems() as $orderItem) { + /** @var ProductInterface $product */ + $product = $orderItem->getProduct(); + if ($product->isBundle()) { + $this->giveBackBundleOrderItem($orderItem); + } else { + $this->giveBackRegularOrderItem($orderItem); + } + } + } + + private function holdBundleOrderItem(OrderItemInterface $orderItem): void + { + foreach ($orderItem->getProductBundleOrderItems() as $bundleOrderItem) { + $quantity = $orderItem->getQuantity() * (int) $bundleOrderItem->getQuantity(); + $variant = $bundleOrderItem->getProductVariant(); + + $this->holdProductVariant($variant, $quantity); + } + } + + private function holdRegularOrderItem(OrderItemInterface $orderItem): void + { + $quantity = $orderItem->getQuantity(); + $variant = $orderItem->getVariant(); + + $this->holdProductVariant($variant, $quantity); + } + + private function holdProductVariant(?ProductVariantInterface $variant, int $quantity): void + { + if (null === $variant) { + throw new \InvalidArgumentException('Variant cannot be null'); + } + + if (!$variant->isTracked()) { + return; + } + + $variant->setOnHold((int) $variant->getOnHold() + $quantity); + } + + private function sellBundleOrderItem(OrderItemInterface $orderItem): void + { + foreach ($orderItem->getProductBundleOrderItems() as $bundleOrderItem) { + $quantity = $orderItem->getQuantity() * (int) $bundleOrderItem->getQuantity(); + $variant = $bundleOrderItem->getProductVariant(); + + $this->sellProductVariant($variant, $quantity); + } + } + + private function sellRegularOrderItem(OrderItemInterface $orderItem): void + { + $quantity = $orderItem->getQuantity(); + $variant = $orderItem->getVariant(); + + $this->sellProductVariant($variant, $quantity); + } + + private function sellProductVariant(?ProductVariantInterface $variant, int $quantity): void + { + if (null === $variant) { + throw new \InvalidArgumentException('Variant cannot be null'); + } + + if (!$variant->isTracked()) { + return; + } + + if (((int) $variant->getOnHold() - $quantity) < 0) { + throw new NotEnoughUnitsOnHoldException((string) $variant->getName()); + } + + if (((int) $variant->getOnHand() - $quantity) < 0) { + throw new NotEnoughUnitsOnHandException((string) $variant->getName()); + } + + $variant->setOnHold((int) $variant->getOnHold() - $quantity); + $variant->setOnHand((int) $variant->getOnHand() - $quantity); + } + + private function releaseBundleOrderItem(OrderItemInterface $orderItem): void + { + foreach ($orderItem->getProductBundleOrderItems() as $bundleOrderItem) { + $quantity = $orderItem->getQuantity() * (int) $bundleOrderItem->getQuantity(); + $variant = $bundleOrderItem->getProductVariant(); + + $this->releaseProductVariant($variant, $quantity); + } + } + + private function releaseRegularOrderItem(OrderItemInterface $orderItem): void + { + $quantity = $orderItem->getQuantity(); + $variant = $orderItem->getVariant(); + + $this->releaseProductVariant($variant, $quantity); + } + + private function releaseProductVariant(?ProductVariantInterface $variant, int $quantity): void + { + if (null === $variant) { + throw new \InvalidArgumentException('Variant cannot be null'); + } + + if (!$variant->isTracked()) { + return; + } + + if (((int) $variant->getOnHold() - $quantity) < 0) { + throw new NotEnoughUnitsOnHoldException((string) $variant->getName()); + } + + $variant->setOnHold((int) $variant->getOnHold() - $quantity); + } + + private function giveBackBundleOrderItem(OrderItemInterface $orderItem): void + { + foreach ($orderItem->getProductBundleOrderItems() as $bundleOrderItem) { + $quantity = $orderItem->getQuantity() * (int) $bundleOrderItem->getQuantity(); + $variant = $bundleOrderItem->getProductVariant(); + + $this->giveBackProductVariant($variant, $quantity); + } + } + + private function giveBackRegularOrderItem(OrderItemInterface $orderItem): void + { + $quantity = $orderItem->getQuantity(); + $variant = $orderItem->getVariant(); + + $this->giveBackProductVariant($variant, $quantity); + } + + private function giveBackProductVariant(?ProductVariantInterface $variant, int $quantity): void + { + if (null === $variant) { + throw new \InvalidArgumentException('Variant cannot be null'); + } + + if (!$variant->isTracked()) { + return; + } + + $variant->setOnHand((int) $variant->getOnHand() + $quantity); + } +} diff --git a/src/Inventory/Operator/ProductBundleOrderInventoryOperatorInterface.php b/src/Inventory/Operator/ProductBundleOrderInventoryOperatorInterface.php new file mode 100644 index 00000000..b196474c --- /dev/null +++ b/src/Inventory/Operator/ProductBundleOrderInventoryOperatorInterface.php @@ -0,0 +1,26 @@ +getProduct(); + if (null === $product->getProductBundle()) { + return; + } + $menu = $event->getMenu(); $menu diff --git a/src/Migrations/Version20240801112208.php b/src/Migrations/Version20240801112208.php new file mode 100644 index 00000000..81d7b79d --- /dev/null +++ b/src/Migrations/Version20240801112208.php @@ -0,0 +1,43 @@ +addSql('CREATE TABLE bitbag_product_bundle (id INT AUTO_INCREMENT NOT NULL, product_id INT NOT NULL, is_packed_product TINYINT(1) NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME DEFAULT NULL, UNIQUE INDEX UNIQ_9EBE7ABF4584665A (product_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE bitbag_product_bundle_item (id INT AUTO_INCREMENT NOT NULL, product_variant_id INT NOT NULL, product_bundle_id INT NOT NULL, quantity INT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME DEFAULT NULL, INDEX IDX_F429FEB6A80EF684 (product_variant_id), INDEX IDX_F429FEB69F5A6F5E (product_bundle_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE bitbag_product_bundle_order_item (id INT AUTO_INCREMENT NOT NULL, product_variant_id INT NOT NULL, order_item_id INT NOT NULL, product_bundle_item_id INT NOT NULL, quantity INT NOT NULL, created_at DATETIME NOT NULL, updated_at DATETIME DEFAULT NULL, INDEX IDX_A615CDA9A80EF684 (product_variant_id), INDEX IDX_A615CDA9E415FB15 (order_item_id), INDEX IDX_A615CDA9B7FE950B (product_bundle_item_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('ALTER TABLE bitbag_product_bundle ADD CONSTRAINT FK_9EBE7ABF4584665A FOREIGN KEY (product_id) REFERENCES sylius_product (id)'); + $this->addSql('ALTER TABLE bitbag_product_bundle_item ADD CONSTRAINT FK_F429FEB6A80EF684 FOREIGN KEY (product_variant_id) REFERENCES sylius_product_variant (id)'); + $this->addSql('ALTER TABLE bitbag_product_bundle_item ADD CONSTRAINT FK_F429FEB69F5A6F5E FOREIGN KEY (product_bundle_id) REFERENCES bitbag_product_bundle (id)'); + $this->addSql('ALTER TABLE bitbag_product_bundle_order_item ADD CONSTRAINT FK_A615CDA9A80EF684 FOREIGN KEY (product_variant_id) REFERENCES sylius_product_variant (id)'); + $this->addSql('ALTER TABLE bitbag_product_bundle_order_item ADD CONSTRAINT FK_A615CDA9E415FB15 FOREIGN KEY (order_item_id) REFERENCES sylius_order_item (id) ON DELETE CASCADE'); + $this->addSql('ALTER TABLE bitbag_product_bundle_order_item ADD CONSTRAINT FK_A615CDA9B7FE950B FOREIGN KEY (product_bundle_item_id) REFERENCES bitbag_product_bundle_item (id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE bitbag_product_bundle DROP FOREIGN KEY FK_9EBE7ABF4584665A'); + $this->addSql('ALTER TABLE bitbag_product_bundle_item DROP FOREIGN KEY FK_F429FEB6A80EF684'); + $this->addSql('ALTER TABLE bitbag_product_bundle_item DROP FOREIGN KEY FK_F429FEB69F5A6F5E'); + $this->addSql('ALTER TABLE bitbag_product_bundle_order_item DROP FOREIGN KEY FK_A615CDA9A80EF684'); + $this->addSql('ALTER TABLE bitbag_product_bundle_order_item DROP FOREIGN KEY FK_A615CDA9E415FB15'); + $this->addSql('ALTER TABLE bitbag_product_bundle_order_item DROP FOREIGN KEY FK_A615CDA9B7FE950B'); + $this->addSql('DROP TABLE bitbag_product_bundle'); + $this->addSql('DROP TABLE bitbag_product_bundle_item'); + $this->addSql('DROP TABLE bitbag_product_bundle_order_item'); + } +} diff --git a/src/Provider/AddProductBundleItemToCartCommandProvider.php b/src/Provider/AddProductBundleItemToCartCommandProvider.php new file mode 100644 index 00000000..5d9137ba --- /dev/null +++ b/src/Provider/AddProductBundleItemToCartCommandProvider.php @@ -0,0 +1,88 @@ + + * + * @throws \Exception + */ + public function provide(string $bundleCode, array $overwrittenVariants): Collection + { + $bundle = $this->productBundleRepository->findOneByProductCode($bundleCode); + if (null === $bundle) { + throw new \Exception('Product bundle not found'); + } + + $bundleItems = $bundle->getProductBundleItems(); + $commands = []; + foreach ($bundleItems as $bundleItem) { + $command = $this->addProductBundleItemToCartCommandFactory->createNew($bundleItem); + if (!$bundle->isPackedProduct() && [] !== $overwrittenVariants) { + $this->overwriteVariant($command, $bundleItem, $overwrittenVariants); + } + $commands[] = $command; + } + + return new ArrayCollection($commands); + } + + private function overwriteVariant( + AddProductBundleItemToCartCommandInterface $command, + ProductBundleItemInterface $bundleItem, + array $overwrittenVariants, + ): void { + foreach ($overwrittenVariants as $overwrittenVariant) { + if (null !== $overwrittenVariant[self::FROM] && null !== $overwrittenVariant[self::TO] && + $bundleItem->getProductVariant()?->getCode() === $overwrittenVariant[self::FROM] && + $this->shouldOverwriteVariant($overwrittenVariant[self::FROM], $overwrittenVariant[self::TO]) + ) { + /** @var ProductVariantInterface $newVariant */ + $newVariant = $this->productVariantRepository->findOneBy(['code' => $overwrittenVariant[self::TO]]); + $command->setProductVariant($newVariant); + } + } + } + + private function shouldOverwriteVariant(string $oldVariantCode, string $newVariantCode): bool + { + $oldVariant = $this->productVariantRepository->findOneBy(['code' => $oldVariantCode]); + $newVariant = $this->productVariantRepository->findOneBy(['code' => $newVariantCode]); + + return + $oldVariant instanceof ProductVariantInterface && + $newVariant instanceof ProductVariantInterface && + $oldVariant->getProduct() === $newVariant->getProduct(); + } +} diff --git a/src/Provider/AddProductBundleItemToCartCommandProviderInterface.php b/src/Provider/AddProductBundleItemToCartCommandProviderInterface.php new file mode 100644 index 00000000..fc355dbf --- /dev/null +++ b/src/Provider/AddProductBundleItemToCartCommandProviderInterface.php @@ -0,0 +1,21 @@ + */ + public function provide(string $bundleCode, array $overwrittenVariants): Collection; +} diff --git a/src/Repository/ProductBundleItemRepository.php b/src/Repository/ProductBundleItemRepository.php new file mode 100644 index 00000000..29995866 --- /dev/null +++ b/src/Repository/ProductBundleItemRepository.php @@ -0,0 +1,31 @@ +createQueryBuilder('pbi') + ->leftJoin('pbi.productBundle', 'pb') + ->leftJoin('pb.product', 'p') + ->where('p.code = :code') + ->setParameter('code', $bundleCode) + ->getQuery() + ->getResult(); + } +} diff --git a/src/Repository/ProductBundleItemRepositoryInterface.php b/src/Repository/ProductBundleItemRepositoryInterface.php new file mode 100644 index 00000000..e431613f --- /dev/null +++ b/src/Repository/ProductBundleItemRepositoryInterface.php @@ -0,0 +1,21 @@ +createQueryBuilder('pb') + ->select('product.id') + ->leftJoin('pb.product', 'product') + ->getQuery() + ->getSingleColumnResult(); + } + + /** @param Collection $variants */ + public function findBundlesByVariants(Collection $variants): array + { + return $this->createQueryBuilder('pb') + ->select('pb') + ->leftJoin('pb.productBundleItems', 'items') + ->andWhere('items.productVariant IN (:variants)') + ->setParameter('variants', $variants) + ->getQuery() + ->getResult(); + } + + public function findOneByProductCode(string $productCode): ?ProductBundleInterface + { + return $this->createQueryBuilder('pb') + ->select('pb') + ->leftJoin('pb.product', 'p') + ->andWhere('p.code = :productCode') + ->setParameter('productCode', $productCode) + ->getQuery() + ->getSingleResult(); + } +} diff --git a/src/Repository/ProductBundleRepositoryInterface.php b/src/Repository/ProductBundleRepositoryInterface.php new file mode 100644 index 00000000..6ee3fde9 --- /dev/null +++ b/src/Repository/ProductBundleRepositoryInterface.php @@ -0,0 +1,27 @@ + $variants */ + public function findBundlesByVariants(Collection $variants): array; + + public function findOneByProductCode(string $productCode): ?ProductBundleInterface; +} diff --git a/src/Repository/ProductVariantRepository.php b/src/Repository/ProductVariantRepository.php deleted file mode 100644 index 8341b5a7..00000000 --- a/src/Repository/ProductVariantRepository.php +++ /dev/null @@ -1,50 +0,0 @@ -getEntityManager()->getExpressionBuilder(); - - return $this->createQueryBuilder('o') - ->leftJoin('o.translations', 'translation', 'WITH', 'translation.locale = :locale') - ->innerJoin('o.product', 'product') - ->andWhere($expr->orX( - 'translation.name LIKE :phrase', - 'o.code LIKE :phrase', - 'product.code LIKE :phrase', - )) - ->setParameter('phrase', '%' . $phrase . '%') - ->setParameter('locale', $locale) - ->setMaxResults(20) - ->getQuery() - ->getResult() - ; - } - - public function findByCodes(array $codes): array - { - return $this->createQueryBuilder('o') - ->andWhere('o.code IN (:codes)') - ->setParameter('codes', $codes) - ->getQuery() - ->getResult() - ; - } -} diff --git a/src/Repository/ProductVariantRepositoryInterface.php b/src/Repository/ProductVariantRepositoryInterface.php deleted file mode 100644 index 739c16f3..00000000 --- a/src/Repository/ProductVariantRepositoryInterface.php +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - admin:order:read - - - - sylius - - - - GET - admin/orders - - - - POST - /shop/orders - input - Sylius\Bundle\ApiBundle\Command\Cart\PickupCart - - shop:order:create - - - Pickups a new cart. Provided locale code has to be one of available for a particular channel. - - - - GET - /shop/orders - - - shop:order:read - - - - - - - - GET - /admin/orders/{tokenValue} - - - - GET - /shop/orders/{tokenValue} - - shop:cart:read - - - - - DELETE - /shop/orders/{tokenValue} - - Deletes cart - - - shop:order:read - - - - - PATCH - /admin/orders/{tokenValue}/cancel - false - sylius.api.order_state_machine_transition_applicator:cancel - - admin:order:update - - - Cancels Order - - - - - PATCH - /shop/orders/{tokenValue}/items - input - Sylius\Bundle\ApiBundle\Command\Cart\AddItemToCart - - shop:cart:read - - - shop:cart:add_item - - - Adds Item to cart - - - - - PATCH - /shop/orders/{tokenValue}/product-bundle - 200 - input - BitBag\SyliusProductBundlePlugin\Dto\Api\AddProductBundleToCartDto - false - - Default - - - shop:cart:add_product_bundle - - - Adds Product Bundle to cart - - - Product bundle added to the cart - - - - - - - PATCH - /shop/orders/{tokenValue}/address - input - Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart - - shop:cart:address - - - shop:cart:read - - - Addresses cart to given location, logged in Customer does not have to provide an email - - - - - PATCH - - sylius - - /shop/orders/{tokenValue}/shipments/{shipmentId} - input - Sylius\Bundle\ApiBundle\Command\Checkout\ChooseShippingMethod - - shop:cart:select_shipping_method - - - shop:cart:read - - - Selects shipping methods for particular shipment - - - tokenValue - path - true - - string - - - - shipmentId - path - true - - string - - - - - - - - PATCH - /shop/orders/{tokenValue}/payments/{paymentId} - input - Sylius\Bundle\ApiBundle\Command\Checkout\ChoosePaymentMethod - - shop:cart:select_payment_method - - - shop:cart:read - - - Selects payment methods for particular payment - - - tokenValue - path - true - - string - - - - paymentId - path - true - - string - - - - - - - - PATCH - /shop/account/orders/{tokenValue}/payments/{paymentId} - input - Sylius\Bundle\ApiBundle\Command\Account\ChangePaymentMethod - - shop:order:account:change_payment_method - - - shop:order:account:read - - - Change the payment method as logged shop user - - - tokenValue - path - true - - string - - - - paymentId - path - true - - string - - - - - - - - GET - sylius.api.get_configuration_action - /shop/orders/{tokenValue}/payments/{paymentId}/configuration - - Retrieve payment method configuration - - - tokenValue - path - true - - string - - - - paymentId - path - true - - string - - - - - - - - PATCH - /shop/orders/{tokenValue}/complete - - sylius - sylius_checkout_complete - - input - Sylius\Bundle\ApiBundle\Command\Checkout\CompleteOrder - - shop:cart:complete - - - shop:cart:read - - - Completes checkout - - - - - DELETE - /shop/orders/{tokenValue}/items/{itemId} - input - Sylius\Bundle\ApiBundle\Controller\DeleteOrderItemAction - false - - shop:cart:remove_item - - - - - tokenValue - path - true - - string - - - - itemId - path - true - - string - - - - - - - - PATCH - /shop/orders/{tokenValue}/items/{orderItemId} - input - Sylius\Bundle\ApiBundle\Command\Cart\ChangeItemQuantityInCart - - shop:cart:change_quantity - - - Changes quantity of order item - - - tokenValue - path - true - - string - - - - orderItemId - path - true - - string - - - - - - - - PATCH - /shop/orders/{tokenValue}/apply-coupon - input - Sylius\Bundle\ApiBundle\Command\Checkout\UpdateCart - - shop:cart:apply_coupon - - - Applies coupon to cart - - - - - PUT - /shop/orders/{tokenValue} - - admin:cart:update - - - - - - - GET - /shop/orders/{tokenValue}/items - - - - GET - /admin/orders/{tokenValue}/shipments - - - - GET - /admin/orders/{tokenValue}/payments - - - - GET - /shop/orders/{tokenValue}/adjustments - - - - GET - /shop/orders/{tokenValue}/payments/{payments}/methods - - - - GET - /shop/orders/{tokenValue}/shipments/{shipments}/methods - - - - GET - /shop/orders/{tokenValue}/items/{items}/adjustments - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Resources/config/api_resources/Order.yml b/src/Resources/config/api_resources/Order.yml new file mode 100644 index 00000000..d6a25203 --- /dev/null +++ b/src/Resources/config/api_resources/Order.yml @@ -0,0 +1,18 @@ +'%sylius.model.order.class%': + shortName: Order + itemOperations: + shop_add_product_bundle: + method: PATCH + path: /shop/orders/{tokenValue}/product-bundle + status: 200 + messenger: input + input: BitBag\SyliusProductBundlePlugin\Dto\Api\AddProductBundleToCartDto + output: false + validation_groups: [ 'Default' ] + denormalization_context: + groups: [ 'shop:cart:add_product_bundle' ] + openapi_context: + summary: Adds Product Bundle to cart + responses: + '200': + description: Product bundle added to the cart diff --git a/src/Resources/config/api_resources/Product.xml b/src/Resources/config/api_resources/Product.xml deleted file mode 100644 index 854c50ed..00000000 --- a/src/Resources/config/api_resources/Product.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - sylius - - - ASC - - - - - GET - /admin/products - - sylius.api.product_name_filter - sylius.api.product_order_filter - sylius.api.product_taxon_code_filter - sylius.api.translation_order_name_and_locale_filter - - - admin:product:read - - - - - GET - /shop/products - - sylius.api.product_name_filter - sylius.api.product_order_filter - sylius.api.product_taxon_code_filter - sylius.api.translation_order_name_and_locale_filter - sylius.api.product_taxon_filter - - - shop:product:read - - - - - POST - /admin/products - - admin:product:create - - - - - - - GET - /admin/products/{code} - - Use code to retrieve a product resource. - - - admin:product:read - - - - - GET - /shop/products/{code} - - Use code to retrieve a product resource. - - - shop:product:read - - - - - PUT - /admin/products/{code} - - admin:product:update - - - - - DELETE - /admin/products/{code} - - - - - - /shop/products/{code}/bundle - - - - - - - - - - object - - - string - string - string - - - - - - - - - - - - - - - - - - diff --git a/src/Resources/config/api_resources/Product.yml b/src/Resources/config/api_resources/Product.yml new file mode 100644 index 00000000..9d46f5e8 --- /dev/null +++ b/src/Resources/config/api_resources/Product.yml @@ -0,0 +1,13 @@ +'%sylius.model.product.class%': + shortName: Product + properties: + productBundle: + writable: false + subresource: + resourceClass: '%bitbag_sylius_product_bundle.model.product_bundle.class%' + collection: false + bundle: + writable: false + subresourceOperations: + product_bundle_get_subresource: + path: /shop/products/{code}/bundle diff --git a/src/Resources/config/config.yml b/src/Resources/config/config.yml index a8236304..76f4bcc0 100644 --- a/src/Resources/config/config.yml +++ b/src/Resources/config/config.yml @@ -1,8 +1,8 @@ imports: - - { resource: resources.yml } - - { resource: services.xml } + - { resource: grids.yml } + - { resource: services.xml } framework: - messenger: - buses: - bitbag_sylius_product_bundle.command_bus: ~ + messenger: + buses: + bitbag_sylius_product_bundle.command_bus: ~ diff --git a/src/Resources/config/doctrine/ProductBundle.orm.xml b/src/Resources/config/doctrine/ProductBundle.orm.xml index ad1214ec..d58497f2 100644 --- a/src/Resources/config/doctrine/ProductBundle.orm.xml +++ b/src/Resources/config/doctrine/ProductBundle.orm.xml @@ -18,16 +18,19 @@ - + - + - + diff --git a/src/Resources/config/doctrine/ProductBundleItem.orm.xml b/src/Resources/config/doctrine/ProductBundleItem.orm.xml index e2645073..2e3a20f4 100644 --- a/src/Resources/config/doctrine/ProductBundleItem.orm.xml +++ b/src/Resources/config/doctrine/ProductBundleItem.orm.xml @@ -3,7 +3,8 @@ - + @@ -18,11 +19,13 @@ - + - - + + diff --git a/src/Resources/config/doctrine/ProductBundleOrderItem.orm.xml b/src/Resources/config/doctrine/ProductBundleOrderItem.orm.xml index 47a6eeb6..389967d1 100644 --- a/src/Resources/config/doctrine/ProductBundleOrderItem.orm.xml +++ b/src/Resources/config/doctrine/ProductBundleOrderItem.orm.xml @@ -3,7 +3,8 @@ - + @@ -18,17 +19,19 @@ - + - + - + diff --git a/src/Resources/config/grids.yml b/src/Resources/config/grids.yml new file mode 100644 index 00000000..27efcc18 --- /dev/null +++ b/src/Resources/config/grids.yml @@ -0,0 +1,46 @@ +sylius_grid: + grids: + sylius_admin_product: + fields: + bundle: + type: twig + label: bitbag_sylius_product_bundle.ui.is_bundle + options: + template: "@SyliusUi/Grid/Field/yesNo.html.twig" + filters: + bundle: + type: is_bundle + label: bitbag_sylius_product_bundle.ui.is_bundle + actions: + main: + create: + type: links + label: sylius.ui.create + options: + class: primary + icon: plus + header: + icon: cube + label: sylius.ui.type + links: + simple: + label: sylius.ui.simple_product + icon: plus + route: sylius_admin_product_create_simple + configurable: + label: sylius.ui.configurable_product + icon: plus + route: sylius_admin_product_create + bundle: + label: bitbag_sylius_product_bundle.ui.bundle + icon: plus + route: bitbag_product_bundle_admin_product_create_bundle + subitem: + variants: + type: product_links + + templates: + filter: + is_bundle: "@SyliusUi/Grid/Filter/boolean.html.twig" + action: + product_links: "@BitBagSyliusProductBundlePlugin/Admin/Grid/Action/product_links.html.twig" diff --git a/src/Resources/config/resources.yml b/src/Resources/config/resources.yml deleted file mode 100644 index 564c562c..00000000 --- a/src/Resources/config/resources.yml +++ /dev/null @@ -1,4 +0,0 @@ -imports: - - { resource: resources/product_bundle.yml } - - { resource: resources/product_bundle_item.yml } - - { resource: resources/product_bundle_order_item.yml } diff --git a/src/Resources/config/resources/product_bundle.yml b/src/Resources/config/resources/product_bundle.yml deleted file mode 100644 index 767218f8..00000000 --- a/src/Resources/config/resources/product_bundle.yml +++ /dev/null @@ -1,8 +0,0 @@ -sylius_resource: - resources: - bitbag_sylius_product_bundle.product_bundle: - driver: doctrine/orm - classes: - model: BitBag\SyliusProductBundlePlugin\Entity\ProductBundle - interface: BitBag\SyliusProductBundlePlugin\Entity\ProductBundleInterface - form: BitBag\SyliusProductBundlePlugin\Form\Type\ProductBundleType diff --git a/src/Resources/config/resources/product_bundle_item.yml b/src/Resources/config/resources/product_bundle_item.yml deleted file mode 100644 index da9bd09e..00000000 --- a/src/Resources/config/resources/product_bundle_item.yml +++ /dev/null @@ -1,8 +0,0 @@ -sylius_resource: - resources: - bitbag_sylius_product_bundle.product_bundle_item: - driver: doctrine/orm - classes: - model: BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItem - interface: BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface - form: BitBag\SyliusProductBundlePlugin\Form\Type\ProductBundleItemType diff --git a/src/Resources/config/resources/product_bundle_order_item.yml b/src/Resources/config/resources/product_bundle_order_item.yml deleted file mode 100644 index c11d81dd..00000000 --- a/src/Resources/config/resources/product_bundle_order_item.yml +++ /dev/null @@ -1,7 +0,0 @@ -sylius_resource: - resources: - bitbag_sylius_product_bundle.product_bundle_order_item: - driver: doctrine/orm - classes: - model: BitBag\SyliusProductBundlePlugin\Entity\ProductBundleOrderItem - interface: BitBag\SyliusProductBundlePlugin\Entity\ProductBundleOrderItemInterface diff --git a/src/Resources/config/routing.yml b/src/Resources/config/routing.yml index 1a87e161..e35d5a36 100644 --- a/src/Resources/config/routing.yml +++ b/src/Resources/config/routing.yml @@ -1,7 +1,7 @@ bitbag_sylius_product_bundle_admin: - resource: routing/admin.yml - prefix: /admin + resource: routing/admin.yml + prefix: /admin bitbag_sylius_product_bundle_shop: - resource: routing/shop.yml - prefix: /{_locale} + resource: routing/shop.yml + prefix: /{_locale} diff --git a/src/Resources/config/routing/admin.yml b/src/Resources/config/routing/admin.yml index a8681f25..547111be 100644 --- a/src/Resources/config/routing/admin.yml +++ b/src/Resources/config/routing/admin.yml @@ -1,51 +1,51 @@ bitbag_product_bundle_admin_product_create_bundle: - path: /products/new/bundle - methods: [GET, POST] - defaults: - _controller: sylius.controller.product::createAction - _sylius: - section: admin - permission: true - factory: - method: createWithVariantAndBundle - template: "@SyliusAdmin/Crud/create.html.twig" - redirect: sylius_admin_product_update - form: - options: - validation_groups: - - sylius - - bitbag_sylius_product_bundle - vars: - subheader: sylius.ui.manage_your_product_catalog - templates: - form: "@SyliusAdmin/Product/_form.html.twig" - route: - name: bitbag_product_bundle_admin_product_create_bundle + path: /products/new/bundle + methods: [ GET, POST ] + defaults: + _controller: sylius.controller.product::createAction + _sylius: + section: admin + permission: true + factory: + method: createWithVariantAndBundle + template: "@SyliusAdmin/Crud/create.html.twig" + redirect: sylius_admin_product_update + form: + options: + validation_groups: + - sylius + - bitbag_sylius_product_bundle + vars: + subheader: sylius.ui.manage_your_product_catalog + templates: + form: "@SyliusAdmin/Product/_form.html.twig" + route: + name: bitbag_product_bundle_admin_product_create_bundle bitbag_product_bundle_admin_ajax_product_variants_by_phrase: - path: /ajax/product-variants/search-by-phrase - methods: [GET] - defaults: - _controller: sylius.controller.product_variant::indexAction - _format: json - _sylius: - serialization_groups: [Autocomplete] - permission: true - repository: - method: findByPhrase - arguments: - phrase: $phrase - locale: expr:service('sylius.context.locale').getLocaleCode() + path: /ajax/product-variants/search-by-phrase + methods: [ GET ] + defaults: + _controller: sylius.controller.product_variant::indexAction + _format: json + _sylius: + serialization_groups: [ Autocomplete ] + permission: true + repository: + method: findByPhrase + arguments: + phrase: $phrase + locale: expr:service('sylius.context.locale').getLocaleCode() bitbag_product_bundle_admin_ajax_product_variants_by_codes: - path: /ajax/product-variants/by-codes - methods: [GET] - defaults: - _controller: sylius.controller.product_variant::indexAction - _format: json - _sylius: - serialization_groups: [Autocomplete] - permission: true - repository: - method: findByCodes - arguments: [$code] + path: /ajax/product-variants/by-codes + methods: [ GET ] + defaults: + _controller: sylius.controller.product_variant::indexAction + _format: json + _sylius: + serialization_groups: [ Autocomplete ] + permission: true + repository: + method: findByCodes + arguments: [ $code ] diff --git a/src/Resources/config/routing/shop.yml b/src/Resources/config/routing/shop.yml index d8d693f4..8d5f3e4b 100644 --- a/src/Resources/config/routing/shop.yml +++ b/src/Resources/config/routing/shop.yml @@ -1,36 +1,36 @@ bitbag_sylius_product_bundle_shop_ajax_cart_add_product_bundle: - path: /ajax/cart/product-bundle/add - methods: [POST] - defaults: - _controller: bitbag_sylius_product_bundle.controller.order_item::addProductBundleAction - _format: json - _sylius: - factory: - method: createForProduct - arguments: [expr:notFoundOnNull(service('sylius.repository.product').find($productId))] - form: - type: BitBag\SyliusProductBundlePlugin\Form\Type\AddProductBundleToCartType - options: - product: expr:notFoundOnNull(service('sylius.repository.product').find($productId)) - redirect: - route: sylius_shop_cart_summary - parameters: {} - flash: sylius.cart.add_item + path: /ajax/cart/product-bundle/add + methods: [ POST ] + defaults: + _controller: bitbag_sylius_product_bundle.controller.order_item::addProductBundleAction + _format: json + _sylius: + factory: + method: createForProduct + arguments: [ expr:notFoundOnNull(service('sylius.repository.product').find($productId)) ] + form: + type: BitBag\SyliusProductBundlePlugin\Form\Type\AddProductBundleToCartType + options: + product: expr:notFoundOnNull(service('sylius.repository.product').find($productId)) + redirect: + route: sylius_shop_cart_summary + parameters: { } + flash: sylius.cart.add_item bitbag_sylius_product_bundle_shop_partial_cart_add_product_bundle: - path: /_partial/cart/product-bundle/add - methods: [GET] - defaults: - _controller: bitbag_sylius_product_bundle.controller.order_item::addProductBundleAction - _sylius: - template: $template - factory: - method: createForProduct - arguments: [expr:notFoundOnNull(service('sylius.repository.product').find($productId))] - form: - type: BitBag\SyliusProductBundlePlugin\Form\Type\AddProductBundleToCartType - options: - product: expr:notFoundOnNull(service('sylius.repository.product').find($productId)) - redirect: - route: sylius_shop_cart_summary - parameters: {} + path: /_partial/cart/product-bundle/add + methods: [ GET ] + defaults: + _controller: bitbag_sylius_product_bundle.controller.order_item::addProductBundleAction + _sylius: + template: $template + factory: + method: createForProduct + arguments: [ expr:notFoundOnNull(service('sylius.repository.product').find($productId)) ] + form: + type: BitBag\SyliusProductBundlePlugin\Form\Type\AddProductBundleToCartType + options: + product: expr:notFoundOnNull(service('sylius.repository.product').find($productId)) + redirect: + route: sylius_shop_cart_summary + parameters: { } diff --git a/src/Resources/config/serialization/AddProductBundleToCartDto.xml b/src/Resources/config/serialization/AddProductBundleToCartDto.xml index 7d6fe771..548434c4 100644 --- a/src/Resources/config/serialization/AddProductBundleToCartDto.xml +++ b/src/Resources/config/serialization/AddProductBundleToCartDto.xml @@ -17,5 +17,8 @@ shop:cart:add_product_bundle + + shop:cart:add_product_bundle + diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 0c4f960c..374239b8 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -1,13 +1,19 @@ - + + + + + + diff --git a/src/Resources/config/services/checker.xml b/src/Resources/config/services/checker.xml new file mode 100644 index 00000000..061b8e8f --- /dev/null +++ b/src/Resources/config/services/checker.xml @@ -0,0 +1,25 @@ + + + + + "false" + + + + + %env(bool:BUNDLED_PRODUCTS_INVENTORY_MANAGEMENT_FEATURE)% + + + + + + + + + + diff --git a/src/Resources/config/services/controller.xml b/src/Resources/config/services/controller.xml index c51fcb10..ec85cc4f 100644 --- a/src/Resources/config/services/controller.xml +++ b/src/Resources/config/services/controller.xml @@ -1,12 +1,15 @@ - + - + - - - + + + diff --git a/src/Resources/config/services/event_listener.xml b/src/Resources/config/services/event_listener.xml index 797d027c..95a149f8 100644 --- a/src/Resources/config/services/event_listener.xml +++ b/src/Resources/config/services/event_listener.xml @@ -2,9 +2,11 @@ - - - + + + diff --git a/src/Resources/config/services/factory.xml b/src/Resources/config/services/factory.xml index 26e912a5..0eff5d4e 100644 --- a/src/Resources/config/services/factory.xml +++ b/src/Resources/config/services/factory.xml @@ -1,17 +1,20 @@ - + - - - + + + - + - + - + diff --git a/src/Resources/config/services/filter.xml b/src/Resources/config/services/filter.xml new file mode 100644 index 00000000..5d9c7bae --- /dev/null +++ b/src/Resources/config/services/filter.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/src/Resources/config/services/fixture.xml b/src/Resources/config/services/fixture.xml new file mode 100644 index 00000000..d86eb6e2 --- /dev/null +++ b/src/Resources/config/services/fixture.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/form.xml b/src/Resources/config/services/form.xml index b64a68d5..06e50700 100644 --- a/src/Resources/config/services/form.xml +++ b/src/Resources/config/services/form.xml @@ -1,6 +1,7 @@ - + bitbag_sylius_product_bundle @@ -12,20 +13,23 @@ - + %bitbag_sylius_product_bundle.model.product_bundle.class% %bitbag_sylius_product_bundle.form.type.product_bundle.validation_groups% - + - + %bitbag_sylius_product_bundle.model.product_bundle_item.class% %bitbag_sylius_product_bundle.form.type.product_bundle_item.validation_groups% - + - - + + diff --git a/src/Resources/config/services/handler.xml b/src/Resources/config/services/handler.xml index 66cfaf68..bbcc3f66 100644 --- a/src/Resources/config/services/handler.xml +++ b/src/Resources/config/services/handler.xml @@ -1,11 +1,13 @@ - + - - - - + + + + diff --git a/src/Resources/config/services/menu.xml b/src/Resources/config/services/menu.xml index 11233699..a95e9905 100644 --- a/src/Resources/config/services/menu.xml +++ b/src/Resources/config/services/menu.xml @@ -1,9 +1,11 @@ - + - - + + diff --git a/src/Resources/config/services/operator.xml b/src/Resources/config/services/operator.xml new file mode 100644 index 00000000..c353aa9e --- /dev/null +++ b/src/Resources/config/services/operator.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/src/Resources/config/services/processor.xml b/src/Resources/config/services/processor.xml index dec8a8cf..ac9c1ead 100644 --- a/src/Resources/config/services/processor.xml +++ b/src/Resources/config/services/processor.xml @@ -1,15 +1,16 @@ - + - - - - + + + + diff --git a/src/Resources/config/services/provider.xml b/src/Resources/config/services/provider.xml new file mode 100644 index 00000000..e04757ca --- /dev/null +++ b/src/Resources/config/services/provider.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/src/Resources/config/services/transformer.xml b/src/Resources/config/services/transformer.xml index 1cacdd6a..ccac7ace 100644 --- a/src/Resources/config/services/transformer.xml +++ b/src/Resources/config/services/transformer.xml @@ -1,12 +1,14 @@ - + - + + diff --git a/src/Resources/config/services/twig.xml b/src/Resources/config/services/twig.xml index e67c3568..8c63e5fc 100644 --- a/src/Resources/config/services/twig.xml +++ b/src/Resources/config/services/twig.xml @@ -1,11 +1,19 @@ - + - - - - + + + + + + + + + diff --git a/src/Resources/config/services/validator.xml b/src/Resources/config/services/validator.xml index 9994250d..89bd7a66 100644 --- a/src/Resources/config/services/validator.xml +++ b/src/Resources/config/services/validator.xml @@ -1,32 +1,38 @@ - + - - - + + + - - + + - + - - + + - + - - + + diff --git a/src/Resources/config/validation/AddProductBundleToCartDto.xml b/src/Resources/config/validation/AddProductBundleToCartDto.xml index 73877de2..293524f1 100644 --- a/src/Resources/config/validation/AddProductBundleToCartDto.xml +++ b/src/Resources/config/validation/AddProductBundleToCartDto.xml @@ -12,6 +12,6 @@ https://symfony.com/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd"> - + diff --git a/src/Resources/translations/messages.en.yml b/src/Resources/translations/messages.en.yml index cecb9d56..ab250ed3 100644 --- a/src/Resources/translations/messages.en.yml +++ b/src/Resources/translations/messages.en.yml @@ -1,8 +1,11 @@ bitbag_sylius_product_bundle: ui: bundle: Bundle + bundles: Bundles product_variant: Product variant quantity: Quantity delete: Delete products_in_bundle: Products in bundle is_packed_product: Is packed product + is_bundle: Is bundle + is_packed_help: If the bundle is not marked as packed, customers will be able to select different variants for products in the bundle diff --git a/src/Resources/views/Admin/Grid/Action/product_links.html.twig b/src/Resources/views/Admin/Grid/Action/product_links.html.twig new file mode 100644 index 00000000..93187fb9 --- /dev/null +++ b/src/Resources/views/Admin/Grid/Action/product_links.html.twig @@ -0,0 +1,31 @@ +{% set visible = options.visible is defined ? options.visible : true %} + +{% if visible %} + {% if not data.bundle %} + + {% else %} + + {% endif %} +{% endif %} diff --git a/src/Resources/views/Admin/Order/Show/Summary/_item.html.twig b/src/Resources/views/Admin/Order/Show/Summary/_item.html.twig new file mode 100644 index 00000000..6ebaedbb --- /dev/null +++ b/src/Resources/views/Admin/Order/Show/Summary/_item.html.twig @@ -0,0 +1,57 @@ +{% import "@SyliusAdmin/Common/Macro/money.html.twig" as money %} + +{% set orderPromotionAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT') %} +{% set unitPromotionAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::ORDER_ITEM_PROMOTION_ADJUSTMENT') %} +{% set shippingAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::SHIPPING_ADJUSTMENT') %} +{% set taxAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::TAX_ADJUSTMENT') %} + +{% set variant = item.variant %} +{% set product = variant.product %} + +{% set aggregatedUnitPromotionAdjustments = item.getAdjustmentsTotalRecursively(unitPromotionAdjustment) + item.getAdjustmentsTotalRecursively(orderPromotionAdjustment) %} +{% set subtotal = (item.unitPrice * item.quantity) + aggregatedUnitPromotionAdjustments %} + +{% set taxIncluded = sylius_admin_order_unit_tax_included(item) %} +{% set taxExcluded = sylius_admin_order_unit_tax_excluded(item) %} + + + + {% include '@SyliusAdmin/Product/_info.html.twig' %} + + + {{ money.format(item.unitPrice, order.currencyCode) }} + + + {{ money.format(item.units.first.adjustmentsTotal(unitPromotionAdjustment), order.currencyCode) }} + + + ~ {{ money.format(item.units.first.adjustmentsTotal(orderPromotionAdjustment), order.currencyCode) }} + + + {{ money.format(item.fullDiscountedUnitPrice, order.currencyCode) }} + + + {{ item.quantity }} + + + {{ money.format(subtotal, order.currencyCode) }} + + +
{{ money.format(taxExcluded, order.currencyCode) }}
+
+
{{ money.format(taxIncluded, order.currencyCode) }} +
+ ({{ 'sylius.ui.included_in_price'|trans }}) +
+ + + {{ money.format(item.total, order.currencyCode) }} + + +{% if product.bundle %} + + + {{ bitbag_render_admin_product_bundle_order_items(item) }} + + +{% endif %} diff --git a/src/Resources/views/Admin/Order/Show/_productBundleOrderItems.html.twig b/src/Resources/views/Admin/Order/Show/_productBundleOrderItems.html.twig index a4ad133f..1d4e667b 100644 --- a/src/Resources/views/Admin/Order/Show/_productBundleOrderItems.html.twig +++ b/src/Resources/views/Admin/Order/Show/_productBundleOrderItems.html.twig @@ -1,35 +1,40 @@ -

{{ 'bitbag_sylius_product_bundle.ui.products_in_bundle'|trans }}

+
+
+ {{ 'bitbag_sylius_product_bundle.ui.products_in_bundle'|trans }} +
-{% for item in items %} - {% set variant = item.productVariant %} - {% set product = variant.product %} +
+ {% for item in items %} + {% set variant = item.productVariant %} + {% set product = variant.product %} -
-
- {% include '@SyliusAdmin/Product/_mainImage.html.twig' with {'product': product, 'filter': 'sylius_admin_product_tiny_thumbnail'} %} -
-
{{ product.name }}
- - {{ variant.code }} - -
-
- {% if product.hasOptions() %} -
- {% for optionValue in variant.optionValues %} -
- {{ optionValue.value }} +
+
+ {% include '@SyliusAdmin/Product/_mainImage.html.twig' with {'product': product, 'filter': 'sylius_admin_product_tiny_thumbnail'} %} +
+
{{ product.name }}
+ + {{ variant.code }} + +
+
+
+ {% if product.hasOptions() %} + {% for optionValue in variant.optionValues %} +
+ {{ optionValue.value }} +
+ {% endfor %} + {% elseif variant.name is not null %} +
+ {{ variant.name }} +
+ {% endif %} +
+ {{ 'bitbag_sylius_product_bundle.ui.quantity'|trans }}: {{ item.quantity }}
- {% endfor %} -
- {% elseif variant.name is not null %} -
-
- {{ variant.name }}
- {% endif %} - -

{{ 'bitbag_sylius_product_bundle.ui.quantity'|trans }}: {{ item.quantity }}

+ {% endfor %}
-{% endfor %} +
diff --git a/src/Resources/views/Admin/Product/Tab/_bundle.html.twig b/src/Resources/views/Admin/Product/Tab/_bundle.html.twig index 586e8568..aa724a8e 100644 --- a/src/Resources/views/Admin/Product/Tab/_bundle.html.twig +++ b/src/Resources/views/Admin/Product/Tab/_bundle.html.twig @@ -3,6 +3,10 @@
{{ form_row(form.productBundle.isPackedProduct) }} + {{ form_help(form.productBundle.isPackedProduct, { + 'help': 'bitbag_sylius_product_bundle.ui.is_packed_help'|trans, + 'help_attr': {'class':'ui info message'} + }) }} {{ form_row(form.productBundle.productBundleItems) }} {{ sonata_block_render_event('sylius.admin.product.' ~ action ~ '.tab_bundle', {'form': form }) }} diff --git a/src/Resources/views/Admin/Product/Update/_toolbar.html.twig b/src/Resources/views/Admin/Product/Update/_toolbar.html.twig new file mode 100644 index 00000000..fbc86335 --- /dev/null +++ b/src/Resources/views/Admin/Product/Update/_toolbar.html.twig @@ -0,0 +1,7 @@ +
+ {% if not product.bundle %} + {% set menu = knp_menu_get('sylius.admin.product.update', [], {'product': product}) %} + {{ knp_menu_render(menu, {'template': '@SyliusUi/Menu/top.html.twig'}) }} + {% endif %} + {% include '@SyliusAdmin/Product/_showInShopButton.html.twig' %} +
diff --git a/src/Resources/views/Admin/Product/show.html.twig b/src/Resources/views/Admin/Product/show.html.twig new file mode 100644 index 00000000..aeb97dc3 --- /dev/null +++ b/src/Resources/views/Admin/Product/show.html.twig @@ -0,0 +1,54 @@ +{% extends '@SyliusAdmin/layout.html.twig' %} + +{% block title %}{{ 'sylius.ui.show_product'|trans }} | {{ product.name }}{% endblock %} + +{% block content %} + {% if product.variants|length == 1 %} + {% include "@SyliusAdmin/Product/Show/_simpleProduct.html.twig" %} + {% else %} + {% include "@SyliusAdmin/Product/Show/_configurableProduct.html.twig" %} + {% endif %} + + {% set bundles = bitbag_get_bundles_containing_product(product) %} + {% if bundles|length > 0 %} + +
+

{{ 'bitbag_sylius_product_bundle.ui.bundles'|trans }}

+ {% for bundle in bundles %} + + {% endfor %} +
+ {% endif %} + + {% if product.isBundle %} + {% set bundledProducts = bitbag_get_products_from_bundle(product) %} + +

{{ 'bitbag_sylius_product_bundle.ui.products_in_bundle'|trans }}

+
+ {% for bundledProduct in bundledProducts %} + {% set variant = bundledProduct.productVariant %} + {% set product = variant.product %} +
+ {% include '@SyliusAdmin/Product/_mainImage.html.twig' with {'product': product, 'filter': 'sylius_admin_product_thumbnail'} %} +
+ + + {{ variant.code }} + + + {{ 'sylius.ui.qty'|trans }}: {{ bundledProduct.quantity }} + +
+
+ {% endfor %} +
+ {% endif %} +{% endblock %} diff --git a/tests/Application/templates/bundles/SyliusAdminBundle/_scripts.html.twig b/src/Resources/views/Admin/_scripts.html.twig similarity index 59% rename from tests/Application/templates/bundles/SyliusAdminBundle/_scripts.html.twig rename to src/Resources/views/Admin/_scripts.html.twig index 54be86fa..ba1e4025 100644 --- a/tests/Application/templates/bundles/SyliusAdminBundle/_scripts.html.twig +++ b/src/Resources/views/Admin/_scripts.html.twig @@ -1,2 +1 @@ -{{ encore_entry_script_tags('admin-entry', null, 'admin') }} {{ encore_entry_script_tags('bitbag-productBundle-admin', null, 'product_bundle_admin') }} diff --git a/tests/Application/templates/bundles/SyliusAdminBundle/_styles.html.twig b/src/Resources/views/Admin/_styles.html.twig similarity index 60% rename from tests/Application/templates/bundles/SyliusAdminBundle/_styles.html.twig rename to src/Resources/views/Admin/_styles.html.twig index d87343da..66909a90 100644 --- a/tests/Application/templates/bundles/SyliusAdminBundle/_styles.html.twig +++ b/src/Resources/views/Admin/_styles.html.twig @@ -1,2 +1 @@ -{{ encore_entry_link_tags('admin-entry', null, 'admin') }} {{ encore_entry_link_tags('bitbag-productBundle-admin', null, 'product_bundle_admin') }} diff --git a/src/Resources/views/Shop/Cart/Summary/_item.html.twig b/src/Resources/views/Shop/Cart/Summary/_item.html.twig new file mode 100644 index 00000000..6ce5b1d7 --- /dev/null +++ b/src/Resources/views/Shop/Cart/Summary/_item.html.twig @@ -0,0 +1,42 @@ +{% import "@SyliusShop/Common/Macro/money.html.twig" as money %} + +{% set product_variant = item.variant %} +{% set original_price_to_display = sylius_order_item_original_price_to_display(item) %} +{% set is_bundle = item.product.bundle %} + + + + {% include '@SyliusShop/Product/_info.html.twig' with {'variant': product_variant} %} + + + {% if original_price_to_display is not null %} + + {{ money.convertAndFormat(original_price_to_display) }} + + {% endif %} + {{ 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_errors(form.quantity) }} + + + +
+ + + +
+ + + {{ money.convertAndFormat(item.subtotal) }} + + +{% if item.product.bundle %} + + + {{ bitbag_render_shop_product_bundle_order_items(item) }} + + +{% endif %} diff --git a/src/Resources/views/Shop/Common/Order/Table/_item.html.twig b/src/Resources/views/Shop/Common/Order/Table/_item.html.twig new file mode 100644 index 00000000..de0185ba --- /dev/null +++ b/src/Resources/views/Shop/Common/Order/Table/_item.html.twig @@ -0,0 +1,31 @@ +{% import "@SyliusShop/Common/Macro/money.html.twig" as money %} + +{% set unitPromotionAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::ORDER_UNIT_PROMOTION_ADJUSTMENT') %} +{% set unitPromotions = item.units.first.adjustments(unitPromotionAdjustment) %} +{% set is_bundle = item.product.bundle %} + + + {% include '@SyliusShop/Product/_info.html.twig' with {'variant': item.variant} %} + + + {% if item.unitPrice != item.discountedUnitPrice %} + {{ money.convertAndFormat(item.unitPrice) }} + {% endif %} + {{ money.convertAndFormat(item.discountedUnitPrice) }} + {% if item.unitPrice != item.discountedUnitPrice %} + + + {% endif %} + + + {{ item.quantity }} + {{ money.convertAndFormat(item.subtotal) }} + +{% if item.product.bundle %} + + + {{ bitbag_render_shop_product_bundle_order_items(item) }} + + +{% endif %} diff --git a/src/Resources/views/Shop/Order/Show/_productBundleOrderItems.html.twig b/src/Resources/views/Shop/Order/Show/_productBundleOrderItems.html.twig new file mode 100644 index 00000000..4e835f98 --- /dev/null +++ b/src/Resources/views/Shop/Order/Show/_productBundleOrderItems.html.twig @@ -0,0 +1,27 @@ +
+
+ {{ 'bitbag_sylius_product_bundle.ui.products_in_bundle'|trans }} +
+ +
+ {% for item in items %} + {% set variant = item.productVariant %} + {% set product = variant.product %} + +
+
+ {% include '@SyliusAdmin/Product/_mainImage.html.twig' with {'product': product, 'filter': 'sylius_admin_product_tiny_thumbnail'} %} +
+
{{ product.name }}
+ + {{ variant.code }} + + + {{ 'sylius.ui.qty'|trans }}: {{ item.quantity }} + +
+
+
+ {% endfor %} +
+
diff --git a/tests/Application/templates/bundles/SyliusShopBundle/Product/Show/_inventory.html.twig b/src/Resources/views/Shop/Product/Show/_inventory.html.twig similarity index 80% rename from tests/Application/templates/bundles/SyliusShopBundle/Product/Show/_inventory.html.twig rename to src/Resources/views/Shop/Product/Show/_inventory.html.twig index 31efa961..6019f8aa 100644 --- a/tests/Application/templates/bundles/SyliusShopBundle/Product/Show/_inventory.html.twig +++ b/src/Resources/views/Shop/Product/Show/_inventory.html.twig @@ -1,4 +1,4 @@ -{% if product.variants.empty() or product.simple and not sylius_inventory_is_available(product.variants.first) %} +{% if product.enabledVariants.empty() or product.simple and not sylius_inventory_is_available(product.enabledVariants.first) %} {% include '@SyliusShop/Product/Show/_outOfStock.html.twig' %} {% else %} {% if product.isBundle %} diff --git a/src/Resources/views/Shop/Product/_variantSelection.html.twig b/src/Resources/views/Shop/Product/_variantSelection.html.twig new file mode 100644 index 00000000..30f96ac7 --- /dev/null +++ b/src/Resources/views/Shop/Product/_variantSelection.html.twig @@ -0,0 +1,7 @@ +{% 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} %} +{% endif %} + +{% include '@BitBagSyliusProductBundlePlugin/Shop/Product/Show/_inventory.html.twig' %} + + diff --git a/tests/Application/templates/bundles/SyliusShopBundle/_scripts.html.twig b/src/Resources/views/Shop/_scripts.html.twig similarity index 60% rename from tests/Application/templates/bundles/SyliusShopBundle/_scripts.html.twig rename to src/Resources/views/Shop/_scripts.html.twig index 278c091e..658945c3 100644 --- a/tests/Application/templates/bundles/SyliusShopBundle/_scripts.html.twig +++ b/src/Resources/views/Shop/_scripts.html.twig @@ -1,2 +1 @@ -{{ encore_entry_script_tags('shop-entry', null, 'shop') }} {{ encore_entry_script_tags('bitbag-productBundle-shop', null, 'product_bundle_shop') }} diff --git a/tests/Application/templates/bundles/SyliusShopBundle/_styles.html.twig b/src/Resources/views/Shop/_styles.html.twig similarity index 60% rename from tests/Application/templates/bundles/SyliusShopBundle/_styles.html.twig rename to src/Resources/views/Shop/_styles.html.twig index 5365a60e..94657919 100644 --- a/tests/Application/templates/bundles/SyliusShopBundle/_styles.html.twig +++ b/src/Resources/views/Shop/_styles.html.twig @@ -1,2 +1 @@ -{{ encore_entry_link_tags('shop-entry', null, 'shop') }} {{ encore_entry_link_tags('bitbag-productBundle-shop', null, 'product_bundle_shop') }} diff --git a/src/Twig/Extension/ProductBundleOrderItemExtension.php b/src/Twig/Extension/ProductBundleOrderItemExtension.php index 5bc1eafd..99d1f1bf 100644 --- a/src/Twig/Extension/ProductBundleOrderItemExtension.php +++ b/src/Twig/Extension/ProductBundleOrderItemExtension.php @@ -21,33 +21,56 @@ final class ProductBundleOrderItemExtension extends AbstractExtension { public function __construct( - private RepositoryInterface $productBundleOrderItemRepository, - private Environment $twig, + private readonly RepositoryInterface $productBundleOrderItemRepository, + private readonly Environment $twig, ) { } public function getFunctions(): array { return [ - new TwigFunction('bitbag_render_product_bundle_order_items', [$this, 'renderProductBundleOrderItems'], ['is_safe' => ['html']]), + new TwigFunction('bitbag_render_admin_product_bundle_order_items', [$this, 'renderAdminProductBundleOrderItems'], ['is_safe' => ['html']]), + new TwigFunction('bitbag_render_shop_product_bundle_order_items', [$this, 'renderShopProductBundleOrderItems'], ['is_safe' => ['html']]), ]; } - public function renderProductBundleOrderItems(OrderItemInterface $orderItem): string + public function renderAdminProductBundleOrderItems(OrderItemInterface $orderItem): string { - /** @var ProductInterface $product */ - $product = $orderItem->getProduct(); + $items = $this->getItems($orderItem); - if (!$product->isBundle()) { + if ([] === $items) { return ''; } - $items = $this->productBundleOrderItemRepository->findBy([ - 'orderItem' => $orderItem, + return $this->twig->render('@BitBagSyliusProductBundlePlugin/Admin/Order/Show/_productBundleOrderItems.html.twig', [ + 'items' => $items, ]); + } - return $this->twig->render('@BitBagSyliusProductBundlePlugin/Admin/Order/Show/_productBundleOrderItems.html.twig', [ + public function renderShopProductBundleOrderItems(OrderItemInterface $orderItem): string + { + $items = $this->getItems($orderItem); + + if ([] === $items) { + return ''; + } + + return $this->twig->render('@BitBagSyliusProductBundlePlugin/Shop/Order/Show/_productBundleOrderItems.html.twig', [ 'items' => $items, ]); } + + private function getItems(OrderItemInterface $orderItem): array + { + /** @var ProductInterface $product */ + $product = $orderItem->getProduct(); + + if (!$product->isBundle()) { + return []; + } + + return $this->productBundleOrderItemRepository->findBy([ + 'orderItem' => $orderItem, + ]); + } } diff --git a/src/Twig/Extension/ProductBundlesExtension.php b/src/Twig/Extension/ProductBundlesExtension.php new file mode 100644 index 00000000..39556207 --- /dev/null +++ b/src/Twig/Extension/ProductBundlesExtension.php @@ -0,0 +1,49 @@ + ['html']]), + new TwigFunction('bitbag_get_products_from_bundle', [$this, 'getProductsFromBundle'], ['is_safe' => ['html']]), + ]; + } + + /** @return ProductBundleInterface[] */ + public function getBundlesForProduct(ProductInterface $product): array + { + return $this->productBundleRepository->findBundlesByVariants($product->getVariants()); + } + + /** @return ProductBundleItemInterface[] */ + public function getProductsFromBundle(ProductInterface $product): array + { + return $this->productBundleItemRepository->findByBundleCode((string) $product->getCode()); + } +} diff --git a/tests/Application/.babelrc b/tests/Application/.babelrc deleted file mode 100644 index e563a62e..00000000 --- a/tests/Application/.babelrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "presets": [ - ["env", { - "targets": { - "node": "6" - }, - "useBuiltIns": true - }] - ], - "plugins": [ - ["transform-object-rest-spread", { - "useBuiltIns": true - }] - ] -} diff --git a/tests/Application/.env b/tests/Application/.env index 6fa8b962..57445681 100644 --- a/tests/Application/.env +++ b/tests/Application/.env @@ -23,6 +23,13 @@ DATABASE_URL=sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db MESSENGER_TRANSPORT_DSN=sync:// ###< symfony/messenger ### +###> sylius/sylius >= 12.4 ### +SYLIUS_MESSENGER_TRANSPORT_MAIN_DSN=${MESSENGER_TRANSPORT_DSN} +SYLIUS_MESSENGER_TRANSPORT_MAIN_FAILED_DSN=${MESSENGER_TRANSPORT_DSN} +SYLIUS_MESSENGER_TRANSPORT_CATALOG_PROMOTION_REMOVAL_DSN=${MESSENGER_TRANSPORT_DSN} +SYLIUS_MESSENGER_TRANSPORT_CATALOG_PROMOTION_REMOVAL_FAILED_DSN=${MESSENGER_TRANSPORT_DSN} +###< sylius/sylius ### + ###> lexik/jwt-authentication-bundle ### JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem @@ -35,3 +42,7 @@ JWT_PASSPHRASE=acme_plugin_development # Delivery is disabled by default via "null://localhost" MAILER_URL=smtp://localhost ###< symfony/swiftmailer-bundle ### + +###> bitbag/product-bundle-plugin ### +BUNDLED_PRODUCTS_INVENTORY_MANAGEMENT_FEATURE=true +###< bitbag/product-bundle-plugin ### diff --git a/tests/Application/config/packages/_sylius.yaml b/tests/Application/config/packages/_sylius.yaml index 5277c18c..e39d353e 100644 --- a/tests/Application/config/packages/_sylius.yaml +++ b/tests/Application/config/packages/_sylius.yaml @@ -1,61 +1,30 @@ imports: - - { resource: "@SyliusCoreBundle/Resources/config/app/config.yml" } + - { resource: "@SyliusCoreBundle/Resources/config/app/config.yml" } - - { resource: "@SyliusAdminBundle/Resources/config/app/config.yml" } + - { resource: "@SyliusAdminBundle/Resources/config/app/config.yml" } - - { resource: "@SyliusShopBundle/Resources/config/app/config.yml" } + - { resource: "@SyliusShopBundle/Resources/config/app/config.yml" } - - { resource: "@SyliusApiBundle/Resources/config/app/config.yaml" } + - { resource: "@SyliusApiBundle/Resources/config/app/config.yaml" } sylius_api: - enabled: true + enabled: true parameters: - sylius_core.public_dir: '%kernel.project_dir%/public' + sylius_core.public_dir: '%kernel.project_dir%/public' sylius_product: - resources: - product: - classes: - model: Tests\BitBag\SyliusProductBundlePlugin\Entity\Product - product_variant: - classes: - repository: BitBag\SyliusProductBundlePlugin\Repository\ProductVariantRepository + resources: + product: + classes: + model: Tests\BitBag\SyliusProductBundlePlugin\Entity\Product sylius_order: - resources: - order_item: - classes: - model: Tests\BitBag\SyliusProductBundlePlugin\Entity\OrderItem + resources: + order_item: + classes: + model: Tests\BitBag\SyliusProductBundlePlugin\Entity\OrderItem sylius_shop: - product_grid: - include_all_descendants: true - -sylius_grid: - grids: - sylius_admin_product: - actions: - main: - create: - type: links - label: sylius.ui.create - options: - class: primary - icon: plus - header: - icon: cube - label: sylius.ui.type - links: - simple: - label: sylius.ui.simple_product - icon: plus - route: sylius_admin_product_create_simple - configurable: - label: sylius.ui.configurable_product - icon: plus - route: sylius_admin_product_create - bundle: - label: bitbag_sylius_product_bundle.ui.bundle - icon: plus - route: bitbag_product_bundle_admin_product_create_bundle + product_grid: + include_all_descendants: true diff --git a/tests/Application/config/packages/bitbag_sylius_product_bundle_plugin.yml b/tests/Application/config/packages/bitbag_sylius_product_bundle_plugin.yml index bd17eb83..71204aee 100644 --- a/tests/Application/config/packages/bitbag_sylius_product_bundle_plugin.yml +++ b/tests/Application/config/packages/bitbag_sylius_product_bundle_plugin.yml @@ -1,2 +1,46 @@ imports: - - { resource: "@BitBagSyliusProductBundlePlugin/Resources/config/config.yml" } + - { resource: "@BitBagSyliusProductBundlePlugin/Resources/config/config.yml" } + +sylius_fixtures: + suites: + default: + fixtures: + tshirt_bundle_products: + name: product + options: + custom: + - name: 'Packed T-Shirt bundle' + tax_category: 'clothing' + channels: + - 'FASHION_WEB' + main_taxon: 'mens_t_shirts' + taxons: + - 't_shirts' + - 'mens_t_shirts' + + - name: 'Not packed T-Shirt bundle' + tax_category: 'clothing' + channels: + - 'FASHION_WEB' + main_taxon: 'womens_t_shirts' + taxons: + - 't_shirts' + - 'womens_t_shirts' + + tshirt_bundles: + name: product_bundle + options: + custom: + - bundle: 'Packed_T_Shirt_bundle' + items: + - 'Sport_basic_white_T_Shirt-variant-0' + - 'Raglan_grey_&_black_Tee-variant-0' + - 'Oversize_white_cotton_T_Shirt-variant-0' + is_packed: true + + - bundle: 'Not_packed_T_Shirt_bundle' + items: + - 'Everyday_white_basic_T_Shirt-variant-0' + - 'Loose_white_designer_T_Shirt-variant-0' + - 'Ribbed_copper_slim_fit_Tee-variant-0' + is_packed: false diff --git a/tests/Application/config/packages/dev/framework.yaml b/tests/Application/config/packages/dev/framework.yaml index 4b116def..1e2ebc1f 100644 --- a/tests/Application/config/packages/dev/framework.yaml +++ b/tests/Application/config/packages/dev/framework.yaml @@ -1,2 +1,2 @@ framework: - profiler: { only_exceptions: false } + profiler: { only_exceptions: false } diff --git a/tests/Application/config/packages/dev/monolog.yaml b/tests/Application/config/packages/dev/monolog.yaml index da2b092d..a8ae3c7d 100644 --- a/tests/Application/config/packages/dev/monolog.yaml +++ b/tests/Application/config/packages/dev/monolog.yaml @@ -1,9 +1,9 @@ monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug - firephp: - type: firephp - level: info + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug + firephp: + type: firephp + level: info diff --git a/tests/Application/config/packages/dev/routing.yaml b/tests/Application/config/packages/dev/routing.yaml index 4116679a..a3d2503d 100644 --- a/tests/Application/config/packages/dev/routing.yaml +++ b/tests/Application/config/packages/dev/routing.yaml @@ -1,3 +1,3 @@ framework: - router: - strict_requirements: true + router: + strict_requirements: true diff --git a/tests/Application/config/packages/dev/web_profiler.yaml b/tests/Application/config/packages/dev/web_profiler.yaml index 1f1cb2bb..be5064e3 100644 --- a/tests/Application/config/packages/dev/web_profiler.yaml +++ b/tests/Application/config/packages/dev/web_profiler.yaml @@ -1,3 +1,3 @@ web_profiler: - toolbar: true - intercept_redirects: false + toolbar: true + intercept_redirects: false diff --git a/tests/Application/config/packages/doctrine.yaml b/tests/Application/config/packages/doctrine.yaml index b4126767..c74b2072 100644 --- a/tests/Application/config/packages/doctrine.yaml +++ b/tests/Application/config/packages/doctrine.yaml @@ -1,25 +1,25 @@ parameters: - # Adds a fallback DATABASE_URL if the env var is not set. - # This allows you to run cache:warmup even if your - # environment variables are not available yet. - # You should not need to change this value. - env(DATABASE_URL): '' + # Adds a fallback DATABASE_URL if the env var is not set. + # This allows you to run cache:warmup even if your + # environment variables are not available yet. + # You should not need to change this value. + env(DATABASE_URL): '' doctrine: - dbal: - driver: 'pdo_mysql' - server_version: '5.7' - charset: UTF8 + dbal: + driver: 'pdo_mysql' + server_version: '5.7' + charset: UTF8 - url: '%env(resolve:DATABASE_URL)%' + url: '%env(resolve:DATABASE_URL)%' - orm: - auto_generate_proxy_classes: '%kernel.debug%' - naming_strategy: doctrine.orm.naming_strategy.underscore - auto_mapping: true - mappings: - App: - is_bundle: false - type: xml - dir: '%kernel.project_dir%/src/Resources/config/doctrine' - prefix: 'Tests\BitBag\SyliusProductBundlePlugin\Entity' + orm: + auto_generate_proxy_classes: '%kernel.debug%' + naming_strategy: doctrine.orm.naming_strategy.underscore + auto_mapping: true + mappings: + App: + is_bundle: false + type: xml + dir: '%kernel.project_dir%/src/Resources/config/doctrine' + prefix: 'Tests\BitBag\SyliusProductBundlePlugin\Entity' diff --git a/tests/Application/config/packages/doctrine_migrations.yaml b/tests/Application/config/packages/doctrine_migrations.yaml index cdbc01ae..2b3bacda 100644 --- a/tests/Application/config/packages/doctrine_migrations.yaml +++ b/tests/Application/config/packages/doctrine_migrations.yaml @@ -1,4 +1,4 @@ doctrine_migrations: - storage: - table_storage: - table_name: sylius_migrations + storage: + table_storage: + table_name: sylius_migrations diff --git a/tests/Application/config/packages/fos_rest.yaml b/tests/Application/config/packages/fos_rest.yaml index a72eef7c..6e54cbdc 100644 --- a/tests/Application/config/packages/fos_rest.yaml +++ b/tests/Application/config/packages/fos_rest.yaml @@ -1,11 +1,11 @@ fos_rest: - exception: true - view: - formats: - json: true - xml: true - empty_content: 204 - format_listener: - rules: - - { path: '^/api/.*', priorities: ['json', 'xml'], fallback_format: json, prefer_extension: true } - - { path: '^/', stop: true } + exception: true + view: + formats: + json: true + xml: true + empty_content: 204 + format_listener: + rules: + - { path: '^/api/.*', priorities: [ 'json', 'xml' ], fallback_format: json, prefer_extension: true } + - { path: '^/', stop: true } diff --git a/tests/Application/config/packages/framework.yaml b/tests/Application/config/packages/framework.yaml index 9b445011..22e9f619 100644 --- a/tests/Application/config/packages/framework.yaml +++ b/tests/Application/config/packages/framework.yaml @@ -1,6 +1,6 @@ framework: - secret: '%env(APP_SECRET)%' - form: true - csrf_protection: true - session: - handler_id: ~ + secret: '%env(APP_SECRET)%' + form: true + csrf_protection: true + session: + handler_id: ~ diff --git a/tests/Application/config/packages/lexik_jwt_authentication.yaml b/tests/Application/config/packages/lexik_jwt_authentication.yaml index edfb69dc..20f1cb0c 100644 --- a/tests/Application/config/packages/lexik_jwt_authentication.yaml +++ b/tests/Application/config/packages/lexik_jwt_authentication.yaml @@ -1,4 +1,4 @@ lexik_jwt_authentication: - secret_key: '%env(resolve:JWT_SECRET_KEY)%' - public_key: '%env(resolve:JWT_PUBLIC_KEY)%' - pass_phrase: '%env(JWT_PASSPHRASE)%' + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + pass_phrase: '%env(JWT_PASSPHRASE)%' diff --git a/tests/Application/config/packages/liip_imagine.yaml b/tests/Application/config/packages/liip_imagine.yaml index bb2e7ceb..28b2e2cf 100644 --- a/tests/Application/config/packages/liip_imagine.yaml +++ b/tests/Application/config/packages/liip_imagine.yaml @@ -1,6 +1,6 @@ liip_imagine: - resolvers: - default: - web_path: - web_root: "%kernel.project_dir%/public" - cache_prefix: "media/cache" + resolvers: + default: + web_path: + web_root: "%kernel.project_dir%/public" + cache_prefix: "media/cache" diff --git a/tests/Application/config/packages/prod/doctrine.yaml b/tests/Application/config/packages/prod/doctrine.yaml index 2f16f0fd..a0f45e57 100644 --- a/tests/Application/config/packages/prod/doctrine.yaml +++ b/tests/Application/config/packages/prod/doctrine.yaml @@ -1,31 +1,31 @@ doctrine: - orm: - metadata_cache_driver: - type: service - id: doctrine.system_cache_provider - query_cache_driver: - type: service - id: doctrine.system_cache_provider - result_cache_driver: - type: service - id: doctrine.result_cache_provider + orm: + metadata_cache_driver: + type: service + id: doctrine.system_cache_provider + query_cache_driver: + type: service + id: doctrine.system_cache_provider + result_cache_driver: + type: service + id: doctrine.result_cache_provider services: - doctrine.result_cache_provider: - class: Symfony\Component\Cache\DoctrineProvider - public: false - arguments: - - '@doctrine.result_cache_pool' - doctrine.system_cache_provider: - class: Symfony\Component\Cache\DoctrineProvider - public: false - arguments: - - '@doctrine.system_cache_pool' + doctrine.result_cache_provider: + class: Symfony\Component\Cache\DoctrineProvider + public: false + arguments: + - '@doctrine.result_cache_pool' + doctrine.system_cache_provider: + class: Symfony\Component\Cache\DoctrineProvider + public: false + arguments: + - '@doctrine.system_cache_pool' framework: - cache: - pools: - doctrine.result_cache_pool: - adapter: cache.app - doctrine.system_cache_pool: - adapter: cache.system + cache: + pools: + doctrine.result_cache_pool: + adapter: cache.app + doctrine.system_cache_pool: + adapter: cache.system diff --git a/tests/Application/config/packages/prod/monolog.yaml b/tests/Application/config/packages/prod/monolog.yaml index 64612114..f17d1996 100644 --- a/tests/Application/config/packages/prod/monolog.yaml +++ b/tests/Application/config/packages/prod/monolog.yaml @@ -1,10 +1,10 @@ monolog: - handlers: - main: - type: fingers_crossed - action_level: error - handler: nested - nested: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug diff --git a/tests/Application/config/packages/routing.yaml b/tests/Application/config/packages/routing.yaml index 368bc7f4..17ae5482 100644 --- a/tests/Application/config/packages/routing.yaml +++ b/tests/Application/config/packages/routing.yaml @@ -1,3 +1,3 @@ framework: - router: - strict_requirements: ~ + router: + strict_requirements: ~ diff --git a/tests/Application/config/packages/staging/monolog.yaml b/tests/Application/config/packages/staging/monolog.yaml index 64612114..f17d1996 100644 --- a/tests/Application/config/packages/staging/monolog.yaml +++ b/tests/Application/config/packages/staging/monolog.yaml @@ -1,10 +1,10 @@ monolog: - handlers: - main: - type: fingers_crossed - action_level: error - handler: nested - nested: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: debug + handlers: + main: + type: fingers_crossed + action_level: error + handler: nested + nested: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug diff --git a/tests/Application/config/packages/stof_doctrine_extensions.yaml b/tests/Application/config/packages/stof_doctrine_extensions.yaml index 7770f74e..b2e32470 100644 --- a/tests/Application/config/packages/stof_doctrine_extensions.yaml +++ b/tests/Application/config/packages/stof_doctrine_extensions.yaml @@ -1,4 +1,4 @@ # Read the documentation: https://symfony.com/doc/current/bundles/StofDoctrineExtensionsBundle/index.html # See the official DoctrineExtensions documentation for more details: https://github.com/Atlantic18/DoctrineExtensions/tree/master/doc/ stof_doctrine_extensions: - default_locale: '%locale%' + default_locale: '%locale%' diff --git a/tests/Application/config/packages/sylius_ui.yaml b/tests/Application/config/packages/sylius_ui.yaml new file mode 100644 index 00000000..2ef12fd6 --- /dev/null +++ b/tests/Application/config/packages/sylius_ui.yaml @@ -0,0 +1,31 @@ +sylius_ui: + events: + sylius.shop.product.show.right_sidebar: + blocks: + variant_selection: + template: "@BitBagSyliusProductBundlePlugin/Shop/Product/_variantSelection.html.twig" + priority: 10 + + sylius.shop.layout.javascripts: + blocks: + plugin_scripts: + template: "@BitBagSyliusProductBundlePlugin/Shop/_scripts.html.twig" + priority: 20 + + sylius.shop.layout.stylesheets: + blocks: + plugin_stylesheets: + template: "@BitBagSyliusProductBundlePlugin/Shop/_styles.html.twig" + priority: 20 + + sylius.admin.layout.javascripts: + blocks: + plugin_scripts: + template: "@BitBagSyliusProductBundlePlugin/Admin/_scripts.html.twig" + priority: 20 + + sylius.admin.layout.stylesheets: + blocks: + plugin_stylesheets: + template: "@BitBagSyliusProductBundlePlugin/Admin/_styles.html.twig" + priority: 20 diff --git a/tests/Application/config/packages/test/framework.yaml b/tests/Application/config/packages/test/framework.yaml index daf04d4e..2734c0ca 100644 --- a/tests/Application/config/packages/test/framework.yaml +++ b/tests/Application/config/packages/test/framework.yaml @@ -1,4 +1,4 @@ framework: - test: ~ - session: - handler_id: ~ + test: ~ + session: + handler_id: ~ diff --git a/tests/Application/config/packages/test/monolog.yaml b/tests/Application/config/packages/test/monolog.yaml index 7e2b9e3a..d5cefe75 100644 --- a/tests/Application/config/packages/test/monolog.yaml +++ b/tests/Application/config/packages/test/monolog.yaml @@ -1,6 +1,6 @@ monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: error + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: error diff --git a/tests/Application/config/packages/test/sylius_theme.yaml b/tests/Application/config/packages/test/sylius_theme.yaml index 4d34199f..3cc3d971 100644 --- a/tests/Application/config/packages/test/sylius_theme.yaml +++ b/tests/Application/config/packages/test/sylius_theme.yaml @@ -1,3 +1,3 @@ sylius_theme: - sources: - test: ~ + sources: + test: ~ diff --git a/tests/Application/config/packages/test/sylius_uploader.yaml b/tests/Application/config/packages/test/sylius_uploader.yaml index ab9d6ca0..cfc5d05b 100644 --- a/tests/Application/config/packages/test/sylius_uploader.yaml +++ b/tests/Application/config/packages/test/sylius_uploader.yaml @@ -1,3 +1,3 @@ services: - Sylius\Component\Core\Generator\ImagePathGeneratorInterface: - class: Sylius\Behat\Service\Generator\UploadedImagePathGenerator + Sylius\Component\Core\Generator\ImagePathGeneratorInterface: + class: Sylius\Behat\Service\Generator\UploadedImagePathGenerator diff --git a/tests/Application/config/packages/test/web_profiler.yaml b/tests/Application/config/packages/test/web_profiler.yaml index 03752de2..808c4cf9 100644 --- a/tests/Application/config/packages/test/web_profiler.yaml +++ b/tests/Application/config/packages/test/web_profiler.yaml @@ -1,6 +1,6 @@ web_profiler: - toolbar: false - intercept_redirects: false + toolbar: false + intercept_redirects: false framework: - profiler: { collect: false } + profiler: { collect: false } diff --git a/tests/Application/config/packages/test_cached/doctrine.yaml b/tests/Application/config/packages/test_cached/doctrine.yaml index 49528606..316da829 100644 --- a/tests/Application/config/packages/test_cached/doctrine.yaml +++ b/tests/Application/config/packages/test_cached/doctrine.yaml @@ -1,16 +1,16 @@ doctrine: - orm: - entity_managers: - default: - result_cache_driver: - type: memcached - host: localhost - port: 11211 - query_cache_driver: - type: memcached - host: localhost - port: 11211 - metadata_cache_driver: - type: memcached - host: localhost - port: 11211 + orm: + entity_managers: + default: + result_cache_driver: + type: memcached + host: localhost + port: 11211 + query_cache_driver: + type: memcached + host: localhost + port: 11211 + metadata_cache_driver: + type: memcached + host: localhost + port: 11211 diff --git a/tests/Application/config/packages/test_cached/fos_rest.yaml b/tests/Application/config/packages/test_cached/fos_rest.yaml index 2b4189da..c36c93f9 100644 --- a/tests/Application/config/packages/test_cached/fos_rest.yaml +++ b/tests/Application/config/packages/test_cached/fos_rest.yaml @@ -1,3 +1,3 @@ fos_rest: - exception: - debug: true + exception: + debug: true diff --git a/tests/Application/config/packages/test_cached/framework.yaml b/tests/Application/config/packages/test_cached/framework.yaml index daf04d4e..2734c0ca 100644 --- a/tests/Application/config/packages/test_cached/framework.yaml +++ b/tests/Application/config/packages/test_cached/framework.yaml @@ -1,4 +1,4 @@ framework: - test: ~ - session: - handler_id: ~ + test: ~ + session: + handler_id: ~ diff --git a/tests/Application/config/packages/test_cached/monolog.yaml b/tests/Application/config/packages/test_cached/monolog.yaml index 7e2b9e3a..d5cefe75 100644 --- a/tests/Application/config/packages/test_cached/monolog.yaml +++ b/tests/Application/config/packages/test_cached/monolog.yaml @@ -1,6 +1,6 @@ monolog: - handlers: - main: - type: stream - path: "%kernel.logs_dir%/%kernel.environment%.log" - level: error + handlers: + main: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: error diff --git a/tests/Application/config/packages/test_cached/sylius_channel.yaml b/tests/Application/config/packages/test_cached/sylius_channel.yaml index bab83ef2..88ff5989 100644 --- a/tests/Application/config/packages/test_cached/sylius_channel.yaml +++ b/tests/Application/config/packages/test_cached/sylius_channel.yaml @@ -1,2 +1,2 @@ sylius_channel: - debug: true + debug: true diff --git a/tests/Application/config/packages/test_cached/sylius_theme.yaml b/tests/Application/config/packages/test_cached/sylius_theme.yaml index 4d34199f..3cc3d971 100644 --- a/tests/Application/config/packages/test_cached/sylius_theme.yaml +++ b/tests/Application/config/packages/test_cached/sylius_theme.yaml @@ -1,3 +1,3 @@ sylius_theme: - sources: - test: ~ + sources: + test: ~ diff --git a/tests/Application/config/packages/test_cached/sylius_uploader.yaml b/tests/Application/config/packages/test_cached/sylius_uploader.yaml index cfa727e2..fcb02b15 100644 --- a/tests/Application/config/packages/test_cached/sylius_uploader.yaml +++ b/tests/Application/config/packages/test_cached/sylius_uploader.yaml @@ -1,2 +1,2 @@ imports: - - { resource: "../test/sylius_uploader.yaml" } + - { resource: "../test/sylius_uploader.yaml" } diff --git a/tests/Application/config/packages/test_cached/twig.yaml b/tests/Application/config/packages/test_cached/twig.yaml index 8c6e0b40..d82243ff 100644 --- a/tests/Application/config/packages/test_cached/twig.yaml +++ b/tests/Application/config/packages/test_cached/twig.yaml @@ -1,2 +1,2 @@ twig: - strict_variables: true + strict_variables: true diff --git a/tests/Application/config/packages/translation.yaml b/tests/Application/config/packages/translation.yaml index 1f4f9664..5b162856 100644 --- a/tests/Application/config/packages/translation.yaml +++ b/tests/Application/config/packages/translation.yaml @@ -1,8 +1,8 @@ framework: - default_locale: '%locale%' - translator: - paths: - - '%kernel.project_dir%/translations' - fallbacks: - - '%locale%' - - 'en' + default_locale: '%locale%' + translator: + paths: + - '%kernel.project_dir%/translations' + fallbacks: + - '%locale%' + - 'en' diff --git a/tests/Application/config/packages/twig.yaml b/tests/Application/config/packages/twig.yaml index 8545473d..0d575c1c 100644 --- a/tests/Application/config/packages/twig.yaml +++ b/tests/Application/config/packages/twig.yaml @@ -1,12 +1,12 @@ twig: - paths: ['%kernel.project_dir%/templates'] - debug: '%kernel.debug%' - strict_variables: '%kernel.debug%' + paths: [ '%kernel.project_dir%/templates' ] + debug: '%kernel.debug%' + strict_variables: '%kernel.debug%' services: - _defaults: - public: false - autowire: true - autoconfigure: true + _defaults: + public: false + autowire: true + autoconfigure: true - Twig\Extra\Intl\IntlExtension: ~ + Twig\Extra\Intl\IntlExtension: ~ diff --git a/tests/Application/config/packages/validator.yaml b/tests/Application/config/packages/validator.yaml index 61807db6..e6553b33 100644 --- a/tests/Application/config/packages/validator.yaml +++ b/tests/Application/config/packages/validator.yaml @@ -1,3 +1,3 @@ framework: - validation: - enable_annotations: true + validation: + enable_annotations: true diff --git a/tests/Application/config/routes.yaml b/tests/Application/config/routes.yaml index 7b7ee8bd..d3e703e0 100644 --- a/tests/Application/config/routes.yaml +++ b/tests/Application/config/routes.yaml @@ -1,2 +1,2 @@ bitbag_sylius_product_bundle_plugin: - resource: "@BitBagSyliusProductBundlePlugin/Resources/config/routing.yml" + resource: "@BitBagSyliusProductBundlePlugin/Resources/config/routing.yml" diff --git a/tests/Application/config/routes/dev/web_profiler.yaml b/tests/Application/config/routes/dev/web_profiler.yaml index 3e79dc21..f54957a8 100644 --- a/tests/Application/config/routes/dev/web_profiler.yaml +++ b/tests/Application/config/routes/dev/web_profiler.yaml @@ -1,7 +1,7 @@ _wdt: - resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" - prefix: /_wdt + resource: "@WebProfilerBundle/Resources/config/routing/wdt.xml" + prefix: /_wdt _profiler: - resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" - prefix: /_profiler + resource: "@WebProfilerBundle/Resources/config/routing/profiler.xml" + prefix: /_profiler diff --git a/tests/Application/config/routes/liip_imagine.yaml b/tests/Application/config/routes/liip_imagine.yaml index 201cbd5d..4e89a350 100644 --- a/tests/Application/config/routes/liip_imagine.yaml +++ b/tests/Application/config/routes/liip_imagine.yaml @@ -1,2 +1,2 @@ _liip_imagine: - resource: "@LiipImagineBundle/Resources/config/routing.yaml" + resource: "@LiipImagineBundle/Resources/config/routing.yaml" diff --git a/tests/Application/config/routes/sylius_admin.yaml b/tests/Application/config/routes/sylius_admin.yaml index 1ba48d6c..baa3afdc 100644 --- a/tests/Application/config/routes/sylius_admin.yaml +++ b/tests/Application/config/routes/sylius_admin.yaml @@ -1,3 +1,3 @@ sylius_admin: - resource: "@SyliusAdminBundle/Resources/config/routing.yml" - prefix: /admin + resource: "@SyliusAdminBundle/Resources/config/routing.yml" + prefix: /admin diff --git a/tests/Application/config/routes/sylius_api.yaml b/tests/Application/config/routes/sylius_api.yaml index ae01ffce..4ed7e8f3 100644 --- a/tests/Application/config/routes/sylius_api.yaml +++ b/tests/Application/config/routes/sylius_api.yaml @@ -1,3 +1,3 @@ sylius_api: - resource: "@SyliusApiBundle/Resources/config/routing.yml" - prefix: "%sylius.security.new_api_route%" + resource: "@SyliusApiBundle/Resources/config/routing.yml" + prefix: "%sylius.security.new_api_route%" diff --git a/tests/Application/config/routes/sylius_shop.yaml b/tests/Application/config/routes/sylius_shop.yaml index fae46cbf..38d83337 100644 --- a/tests/Application/config/routes/sylius_shop.yaml +++ b/tests/Application/config/routes/sylius_shop.yaml @@ -1,14 +1,14 @@ sylius_shop: - resource: "@SyliusShopBundle/Resources/config/routing.yml" - prefix: /{_locale} - requirements: - _locale: ^[A-Za-z]{2,4}(_([A-Za-z]{4}|[0-9]{3}))?(_([A-Za-z]{2}|[0-9]{3}))?$ + resource: "@SyliusShopBundle/Resources/config/routing.yml" + prefix: /{_locale} + requirements: + _locale: ^[A-Za-z]{2,4}(_([A-Za-z]{4}|[0-9]{3}))?(_([A-Za-z]{2}|[0-9]{3}))?$ sylius_shop_payum: - resource: "@SyliusShopBundle/Resources/config/routing/payum.yml" + resource: "@SyliusShopBundle/Resources/config/routing/payum.yml" sylius_shop_default_locale: - path: / - methods: [GET] - defaults: - _controller: sylius.controller.shop.locale_switch::switchAction + path: / + methods: [ GET ] + defaults: + _controller: sylius.controller.shop.locale_switch::switchAction diff --git a/tests/Application/config/routes/test/sylius_test_plugin.yaml b/tests/Application/config/routes/test/sylius_test_plugin.yaml index 0ca57d9a..138859f8 100644 --- a/tests/Application/config/routes/test/sylius_test_plugin.yaml +++ b/tests/Application/config/routes/test/sylius_test_plugin.yaml @@ -1,5 +1,5 @@ sylius_test_plugin_main: - path: /test/main - controller: FrameworkBundle:Template:template - defaults: - template: "@SyliusTestPlugin/main.html.twig" + path: /test/main + controller: FrameworkBundle:Template:template + defaults: + template: "@SyliusTestPlugin/main.html.twig" diff --git a/tests/Application/config/routes/test_cached/routing.yaml b/tests/Application/config/routes/test_cached/routing.yaml index 0ca57d9a..138859f8 100644 --- a/tests/Application/config/routes/test_cached/routing.yaml +++ b/tests/Application/config/routes/test_cached/routing.yaml @@ -1,5 +1,5 @@ sylius_test_plugin_main: - path: /test/main - controller: FrameworkBundle:Template:template - defaults: - template: "@SyliusTestPlugin/main.html.twig" + path: /test/main + controller: FrameworkBundle:Template:template + defaults: + template: "@SyliusTestPlugin/main.html.twig" diff --git a/tests/Application/config/routes/test_cached/sylius_test_plugin.yaml b/tests/Application/config/routes/test_cached/sylius_test_plugin.yaml index 0ca57d9a..138859f8 100644 --- a/tests/Application/config/routes/test_cached/sylius_test_plugin.yaml +++ b/tests/Application/config/routes/test_cached/sylius_test_plugin.yaml @@ -1,5 +1,5 @@ sylius_test_plugin_main: - path: /test/main - controller: FrameworkBundle:Template:template - defaults: - template: "@SyliusTestPlugin/main.html.twig" + path: /test/main + controller: FrameworkBundle:Template:template + defaults: + template: "@SyliusTestPlugin/main.html.twig" diff --git a/tests/Application/config/services.yaml b/tests/Application/config/services.yaml index 615506eb..ee3b8248 100644 --- a/tests/Application/config/services.yaml +++ b/tests/Application/config/services.yaml @@ -1,4 +1,4 @@ # 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 + locale: en_US diff --git a/tests/Application/config/services_test.yaml b/tests/Application/config/services_test.yaml index 9edf553d..f90fe420 100644 --- a/tests/Application/config/services_test.yaml +++ b/tests/Application/config/services_test.yaml @@ -1,8 +1,8 @@ imports: - - { resource: "../../Behat/Resources/services.yml" } - - { resource: "../../../vendor/sylius/sylius/src/Sylius/Behat/Resources/config/services.xml" } + - { resource: "../../Behat/Resources/services.yml" } + - { resource: "../../../vendor/sylius/sylius/src/Sylius/Behat/Resources/config/services.xml" } # workaround needed for strange "test.client.history" problem # see https://github.com/FriendsOfBehat/SymfonyExtension/issues/88 services: - Symfony\Component\BrowserKit\AbstractBrowser: '@test.client' + Symfony\Component\BrowserKit\AbstractBrowser: '@test.client' diff --git a/tests/Application/config/services_test_cached.yaml b/tests/Application/config/services_test_cached.yaml index 0de380ea..5d2c7fa4 100644 --- a/tests/Application/config/services_test_cached.yaml +++ b/tests/Application/config/services_test_cached.yaml @@ -1,2 +1,2 @@ imports: - - { resource: "services_test.yaml" } + - { resource: "services_test.yaml" } diff --git a/tests/Application/config/sylius/1.12/packages/jms_serializer.yaml b/tests/Application/config/sylius/1.12/packages/jms_serializer.yaml index ed7bc613..fa43f5b6 100644 --- a/tests/Application/config/sylius/1.12/packages/jms_serializer.yaml +++ b/tests/Application/config/sylius/1.12/packages/jms_serializer.yaml @@ -1,4 +1,4 @@ jms_serializer: - visitors: - xml_serialization: - format_output: '%kernel.debug%' + visitors: + xml_serialization: + format_output: '%kernel.debug%' diff --git a/tests/Application/config/sylius/1.12/packages/security.yaml b/tests/Application/config/sylius/1.12/packages/security.yaml index 2b277f55..cb926ec4 100644 --- a/tests/Application/config/sylius/1.12/packages/security.yaml +++ b/tests/Application/config/sylius/1.12/packages/security.yaml @@ -113,9 +113,9 @@ security: security: false access_control: - - { path: "%sylius.security.admin_regex%/_partial", role: PUBLIC_ACCESS, ips: [127.0.0.1, ::1] } + - { path: "%sylius.security.admin_regex%/_partial", role: PUBLIC_ACCESS, ips: [ 127.0.0.1, ::1 ] } - { path: "%sylius.security.admin_regex%/_partial", role: ROLE_NO_ACCESS } - - { path: "%sylius.security.shop_regex%/_partial", role: PUBLIC_ACCESS, ips: [127.0.0.1, ::1] } + - { path: "%sylius.security.shop_regex%/_partial", role: PUBLIC_ACCESS, ips: [ 127.0.0.1, ::1 ] } - { path: "%sylius.security.shop_regex%/_partial", role: ROLE_NO_ACCESS } - { path: "%sylius.security.admin_regex%/login", role: PUBLIC_ACCESS } diff --git a/tests/Application/config/sylius/1.13/packages/jms_serializer.yaml b/tests/Application/config/sylius/1.13/packages/jms_serializer.yaml index ed7bc613..fa43f5b6 100644 --- a/tests/Application/config/sylius/1.13/packages/jms_serializer.yaml +++ b/tests/Application/config/sylius/1.13/packages/jms_serializer.yaml @@ -1,4 +1,4 @@ jms_serializer: - visitors: - xml_serialization: - format_output: '%kernel.debug%' + visitors: + xml_serialization: + format_output: '%kernel.debug%' diff --git a/tests/Application/config/sylius/1.13/packages/security.yaml b/tests/Application/config/sylius/1.13/packages/security.yaml index 7458c77a..6df8cb5a 100644 --- a/tests/Application/config/sylius/1.13/packages/security.yaml +++ b/tests/Application/config/sylius/1.13/packages/security.yaml @@ -102,9 +102,9 @@ security: security: false access_control: - - { path: "%sylius.security.admin_regex%/_partial", role: PUBLIC_ACCESS, ips: [127.0.0.1, ::1] } + - { path: "%sylius.security.admin_regex%/_partial", role: PUBLIC_ACCESS, ips: [ 127.0.0.1, ::1 ] } - { path: "%sylius.security.admin_regex%/_partial", role: ROLE_NO_ACCESS } - - { path: "%sylius.security.shop_regex%/_partial", role: PUBLIC_ACCESS, ips: [127.0.0.1, ::1] } + - { path: "%sylius.security.shop_regex%/_partial", role: PUBLIC_ACCESS, ips: [ 127.0.0.1, ::1 ] } - { path: "%sylius.security.shop_regex%/_partial", role: ROLE_NO_ACCESS } - { path: "%sylius.security.admin_regex%/login", role: PUBLIC_ACCESS } diff --git a/tests/Application/config/symfony/4.4/packages/framework.yaml b/tests/Application/config/symfony/4.4/packages/framework.yaml index 62f82d35..4cc0d5ae 100644 --- a/tests/Application/config/symfony/4.4/packages/framework.yaml +++ b/tests/Application/config/symfony/4.4/packages/framework.yaml @@ -1,2 +1,2 @@ framework: - templating: { engines: ["twig"] } + templating: { engines: [ "twig" ] } diff --git a/tests/Application/gulpfile.babel.js b/tests/Application/gulpfile.babel.js deleted file mode 100644 index 5920316f..00000000 --- a/tests/Application/gulpfile.babel.js +++ /dev/null @@ -1,60 +0,0 @@ -import chug from 'gulp-chug'; -import gulp from 'gulp'; -import yargs from 'yargs'; - -const { argv } = yargs - .options({ - rootPath: { - description: ' path to public assets directory', - type: 'string', - requiresArg: true, - required: false, - }, - nodeModulesPath: { - description: ' path to node_modules directory', - type: 'string', - requiresArg: true, - required: false, - }, - }); - -const config = [ - '--rootPath', - argv.rootPath || '../../../../../../../tests/Application/public/assets', - '--nodeModulesPath', - argv.nodeModulesPath || '../../../../../../../tests/Application/node_modules', -]; - -export const buildAdmin = function buildAdmin() { - return gulp.src('../../vendor/sylius/sylius/src/Sylius/Bundle/AdminBundle/gulpfile.babel.js', { read: false }) - .pipe(chug({ args: config, tasks: 'build' })); -}; -buildAdmin.description = 'Build admin assets.'; - -export const watchAdmin = function watchAdmin() { - return gulp.src('../../vendor/sylius/sylius/src/Sylius/Bundle/AdminBundle/gulpfile.babel.js', { read: false }) - .pipe(chug({ args: config, tasks: 'watch' })); -}; -watchAdmin.description = 'Watch admin asset sources and rebuild on changes.'; - -export const buildShop = function buildShop() { - return gulp.src('../../vendor/sylius/sylius/src/Sylius/Bundle/ShopBundle/gulpfile.babel.js', { read: false }) - .pipe(chug({ args: config, tasks: 'build' })); -}; -buildShop.description = 'Build shop assets.'; - -export const watchShop = function watchShop() { - return gulp.src('../../vendor/sylius/sylius/src/Sylius/Bundle/ShopBundle/gulpfile.babel.js', { read: false }) - .pipe(chug({ args: config, tasks: 'watch' })); -}; -watchShop.description = 'Watch shop asset sources and rebuild on changes.'; - -export const build = gulp.parallel(buildAdmin, buildShop); -build.description = 'Build assets.'; - -gulp.task('admin', buildAdmin); -gulp.task('admin-watch', watchAdmin); -gulp.task('shop', buildShop); -gulp.task('shop-watch', watchShop); - -export default build; diff --git a/tests/Application/package.json b/tests/Application/package.json index 16de6eea..dfe32583 100644 --- a/tests/Application/package.json +++ b/tests/Application/package.json @@ -1,44 +1,13 @@ { - "dependencies": { - "babel-polyfill": "^6.26.0", - "jquery": "^3.4.0", - "jquery.dirtyforms": "^2.0.0", - "lightbox2": "^2.9.0", - "semantic-ui-css": "^2.2.0", - "slick-carousel": "^1.8.1", - "chart.js": "^3.7.1" - }, - "devDependencies": { - "@symfony/webpack-encore": "^1.6.1", - "babel-core": "^6.26.3", - "babel-plugin-external-helpers": "^6.22.0", - "babel-plugin-module-resolver": "^3.1.1", - "babel-plugin-transform-object-rest-spread": "^6.26.0", - "babel-preset-env": "^1.7.0", - "babel-register": "^6.26.0", - "dedent": "^0.7.0", - "eslint": "^4.19.1", - "eslint-config-airbnb-base": "^12.1.0", - "eslint-import-resolver-babel-module": "^4.0.0", - "eslint-plugin-import": "^2.11.0", - "fast-async": "^6.3.7", - "merge-stream": "^1.0.0", - "sass": "^1.50.0", - "sass-loader": "^7.0.1", - "upath": "^1.1.0", - "webpack": "^5.76.1", - "yargs": "^6.4.0" - }, + "license": "MIT", "scripts": { - "build": "encore production", - "dev": "encore dev", + "build": "encore dev", + "build:prod": "encore production", + "postinstall": "semantic-ui-css-patch", "lint": "yarn lint:js", - "lint:js": "eslint gulpfile.babel.js" + "watch": "encore dev --watch" }, - "repository": { - "type": "git", - "url": "git+https://github.com/Sylius/Sylius.git" - }, - "author": "Paweł Jędrzejewski", - "license": "MIT" + "devDependencies": { + "@sylius-ui/frontend": "^1.0" + } } diff --git a/tests/Application/src/Resources/config/doctrine/OrderItem.orm.xml b/tests/Application/src/Resources/config/doctrine/OrderItem.orm.xml index 5f6eaa93..b87772f8 100644 --- a/tests/Application/src/Resources/config/doctrine/OrderItem.orm.xml +++ b/tests/Application/src/Resources/config/doctrine/OrderItem.orm.xml @@ -5,7 +5,9 @@ http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd" > - + diff --git a/tests/Application/src/Resources/config/doctrine/Product.orm.xml b/tests/Application/src/Resources/config/doctrine/Product.orm.xml index 974072b7..d291029b 100644 --- a/tests/Application/src/Resources/config/doctrine/Product.orm.xml +++ b/tests/Application/src/Resources/config/doctrine/Product.orm.xml @@ -5,7 +5,8 @@ http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd" > - + diff --git a/tests/Application/templates/bundles/SyliusAdminBundle/Layout/_logo.html.twig b/tests/Application/templates/bundles/SyliusAdminBundle/Layout/_logo.html.twig deleted file mode 100644 index 1d9fa7d0..00000000 --- a/tests/Application/templates/bundles/SyliusAdminBundle/Layout/_logo.html.twig +++ /dev/null @@ -1,5 +0,0 @@ - -
- -
-
diff --git a/tests/Application/templates/bundles/SyliusAdminBundle/Order/Show/Summary/_item.html.twig b/tests/Application/templates/bundles/SyliusAdminBundle/Order/Show/Summary/_item.html.twig index 6d94509c..3c48d043 100644 --- a/tests/Application/templates/bundles/SyliusAdminBundle/Order/Show/Summary/_item.html.twig +++ b/tests/Application/templates/bundles/SyliusAdminBundle/Order/Show/Summary/_item.html.twig @@ -1,38 +1 @@ -{% import "@SyliusAdmin/Common/Macro/money.html.twig" as money %} - -{% set orderPromotionAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::ORDER_PROMOTION_ADJUSTMENT') %} -{% set itemPromotionAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::ORDER_ITEM_PROMOTION_ADJUSTMENT') %} -{% set shippingAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::SHIPPING_ADJUSTMENT') %} -{% set taxAdjustment = constant('Sylius\\Component\\Core\\Model\\AdjustmentInterface::TAX_ADJUSTMENT') %} - -{% set variant = item.variant %} -{% set product = variant.product %} - - - - {% include '@SyliusAdmin/Product/_info.html.twig' %} - - {{ bitbag_render_product_bundle_order_items(item) }} - - - {{ money.format(item.unitPrice, order.currencyCode) }} - - - {{ money.format(item.discountedUnitPrice, order.currencyCode) }} - - - {{ item.quantity }} - - - {{ money.format(item.subtotal, order.currencyCode) }} - - - {{ money.format(item.getAdjustmentsTotalRecursively(orderPromotionAdjustment), order.currencyCode) }} - - - {{ money.format(item.taxTotal, order.currencyCode) }} - - - {{ money.format(item.total, order.currencyCode) }} - - +{% include '@BitBagSyliusProductBundlePlugin/Admin/Order/Show/Summary/_item.html.twig' %} diff --git a/tests/Application/templates/bundles/SyliusAdminBundle/Product/Update/_toolbar.html.twig b/tests/Application/templates/bundles/SyliusAdminBundle/Product/Update/_toolbar.html.twig new file mode 100644 index 00000000..ab8bbcf4 --- /dev/null +++ b/tests/Application/templates/bundles/SyliusAdminBundle/Product/Update/_toolbar.html.twig @@ -0,0 +1 @@ +{% include '@BitBagSyliusProductBundlePlugin/Admin/Product/Update/_toolbar.html.twig' %} diff --git a/tests/Application/templates/bundles/SyliusAdminBundle/Product/show.html.twig b/tests/Application/templates/bundles/SyliusAdminBundle/Product/show.html.twig new file mode 100644 index 00000000..054123dd --- /dev/null +++ b/tests/Application/templates/bundles/SyliusAdminBundle/Product/show.html.twig @@ -0,0 +1 @@ +{% include '@BitBagSyliusProductBundlePlugin/Admin/Product/show.html.twig' %} diff --git a/tests/Application/templates/bundles/SyliusShopBundle/Cart/Summary/_item.html.twig b/tests/Application/templates/bundles/SyliusShopBundle/Cart/Summary/_item.html.twig new file mode 100644 index 00000000..36dd5077 --- /dev/null +++ b/tests/Application/templates/bundles/SyliusShopBundle/Cart/Summary/_item.html.twig @@ -0,0 +1 @@ +{% include '@BitBagSyliusProductBundlePlugin/Shop/Cart/Summary/_item.html.twig' %} diff --git a/tests/Application/templates/bundles/SyliusShopBundle/Common/Order/Table/_item.html.twig b/tests/Application/templates/bundles/SyliusShopBundle/Common/Order/Table/_item.html.twig new file mode 100644 index 00000000..271a5aac --- /dev/null +++ b/tests/Application/templates/bundles/SyliusShopBundle/Common/Order/Table/_item.html.twig @@ -0,0 +1 @@ +{% include '@BitBagSyliusProductBundlePlugin/Shop/Common/Order/Table/_item.html.twig' %} diff --git a/tests/Application/templates/bundles/SyliusShopBundle/Layout/Header/_logo.html.twig b/tests/Application/templates/bundles/SyliusShopBundle/Layout/Header/_logo.html.twig deleted file mode 100644 index 84b8df56..00000000 --- a/tests/Application/templates/bundles/SyliusShopBundle/Layout/Header/_logo.html.twig +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/tests/Behat/Context/Api/ProductBundleContext.php b/tests/Behat/Context/Api/ProductBundleContext.php new file mode 100644 index 00000000..d408136c --- /dev/null +++ b/tests/Behat/Context/Api/ProductBundleContext.php @@ -0,0 +1,155 @@ +requestFactory->customItemAction( + 'shop', + Resources::ORDERS, + $this->sharedStorage->get('cart_token'), + HttpRequest::METHOD_PATCH, + 'product-bundle', + ); + $request->updateContent([ + 'productCode' => $product->getCode(), + 'quantity' => $quantity, + ]); + + $this->client->executeCustomRequest($request); + } + + /** + * @When I add bundle :product with quantity :quantity to my cart and overwrite :oldVariant with :newVariant + */ + public function iAddProductBundleToMyCartAndOverwriteVariant( + ProductInterface $product, + int $quantity, + string $oldVariant, + string $newVariant, + ): void { + $request = $this->requestFactory->customItemAction( + 'shop', + Resources::ORDERS, + $this->sharedStorage->get('cart_token'), + HttpRequest::METHOD_PATCH, + 'product-bundle', + ); + $request->updateContent([ + 'productCode' => $product->getCode(), + 'quantity' => $quantity, + 'overwrittenVariants' => [ + [ + AddProductBundleItemToCartCommandProvider::FROM => $oldVariant, + AddProductBundleItemToCartCommandProvider::TO => $newVariant, + ], + ], + ]); + + $this->client->executeCustomRequest($request); + } + + /** + * @When I should have bundle :product with quantity :quantity in my cart + */ + public function iShouldHaveBundleWithQuantityInMyCart(ProductInterface $product, int $quantity): void + { + $response = $this->client->show(Resources::ORDERS, $this->sharedStorage->get('cart_token')); + + $item = $this->responseChecker->getValue($response, 'items')[0]; + Assert::eq($item['productName'], $product->getName()); + Assert::eq($item['quantity'], $quantity); + } + + /** + * @When I should have product :product in bundled items + */ + public function iShouldHaveProductInBundledItems(ProductInterface $product): void + { + $response = $this->client->show(Resources::ORDERS, $this->sharedStorage->get('cart_token')); + + $productBundleOrderItems = $this->responseChecker->getValue($response, 'items')[0]['productBundleOrderItems']; + foreach ($productBundleOrderItems as $item) { + if ($item['productVariant']['code'] === $product->getCode()) { + return; + } + } + + throw new \InvalidArgumentException('Product not found in bundled items'); + } + + /** + * @When I should have product variant :productVariant in bundled items + */ + public function iShouldHaveProductVariantInBundledItems(string $productVariant): void + { + $productVariant = $this->productVariantRepository->findOneBy(['code' => $productVariant]); + Assert::isInstanceOf($productVariant, ProductVariantInterface::class); + + $response = $this->client->show(Resources::ORDERS, $this->sharedStorage->get('cart_token')); + + $productBundleOrderItems = $this->responseChecker->getValue($response, 'items')[0]['productBundleOrderItems']; + foreach ($productBundleOrderItems as $item) { + if ($item['productVariant']['code'] === $productVariant->getCode()) { + return; + } + } + + throw new \InvalidArgumentException('Product not found in bundled items'); + } + + /** + * @When I should not have product variant :productVariant in bundled items + */ + public function iShouldNotHaveProductVariantInBundledItems(string $productVariant): void + { + $productVariant = $this->productVariantRepository->findOneBy(['code' => $productVariant]); + Assert::isInstanceOf($productVariant, ProductVariantInterface::class); + + $response = $this->client->show(Resources::ORDERS, $this->sharedStorage->get('cart_token')); + + $productBundleOrderItems = $this->responseChecker->getValue($response, 'items')[0]['productBundleOrderItems']; + foreach ($productBundleOrderItems as $item) { + if ($item['productVariant']['code'] === $productVariant->getCode()) { + throw new \InvalidArgumentException(\sprintf('Product variant %s found in bundled items', $productVariant->getName())); + } + } + } +} diff --git a/tests/Behat/Context/Setup/ProductBundleContext.php b/tests/Behat/Context/Setup/ProductBundleContext.php index ab406ff0..6b689bd2 100644 --- a/tests/Behat/Context/Setup/ProductBundleContext.php +++ b/tests/Behat/Context/Setup/ProductBundleContext.php @@ -14,16 +14,22 @@ use Behat\Behat\Context\Context; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductInterface; +use BitBag\SyliusProductBundlePlugin\Factory\OrderItemFactoryInterface; +use BitBag\SyliusProductBundlePlugin\Factory\ProductBundleOrderItemFactoryInterface; use BitBag\SyliusProductBundlePlugin\Factory\ProductFactory; +use BitBag\SyliusProductBundlePlugin\Repository\ProductBundleRepositoryInterface; use Doctrine\ORM\EntityManagerInterface; use Sylius\Behat\Service\SharedStorageInterface; use Sylius\Component\Core\Formatter\StringInflector; use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ChannelPricingInterface; +use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\ProductTaxonInterface; use Sylius\Component\Core\Model\ProductVariantInterface; use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Core\Repository\ProductRepositoryInterface; +use Sylius\Component\Order\Modifier\OrderItemQuantityModifierInterface; +use Sylius\Component\Order\Modifier\OrderModifierInterface; use Sylius\Component\Product\Generator\SlugGeneratorInterface; use Sylius\Component\Product\Resolver\ProductVariantResolverInterface; use Sylius\Component\Resource\Factory\FactoryInterface; @@ -31,16 +37,22 @@ final class ProductBundleContext implements Context { public function __construct( - private SharedStorageInterface $sharedStorage, - private FactoryInterface $taxonFactory, - private ProductRepositoryInterface $productRepository, - private FactoryInterface $productTaxonFactory, - private EntityManagerInterface $productTaxonManager, - private ProductFactory $productFactory, - private FactoryInterface $productBundleItemFactory, - private FactoryInterface $channelPricingFactory, - private ProductVariantResolverInterface $productVariantResolver, - private SlugGeneratorInterface $slugGenerator, + private readonly SharedStorageInterface $sharedStorage, + private readonly FactoryInterface $taxonFactory, + private readonly ProductRepositoryInterface $productRepository, + private readonly FactoryInterface $productTaxonFactory, + private readonly EntityManagerInterface $productTaxonManager, + private readonly ProductFactory $productFactory, + private readonly FactoryInterface $productBundleItemFactory, + private readonly FactoryInterface $channelPricingFactory, + private readonly ProductVariantResolverInterface $productVariantResolver, + private readonly SlugGeneratorInterface $slugGenerator, + private readonly EntityManagerInterface $objectManager, + private readonly OrderItemQuantityModifierInterface $orderItemQuantityModifier, + private readonly ProductBundleOrderItemFactoryInterface $productBundleOrderItemFactory, + private readonly OrderModifierInterface $orderModifier, + private readonly OrderItemFactoryInterface $cartItemFactory, + private readonly ProductBundleRepositoryInterface $productBundleRepository, ) { } @@ -149,4 +161,37 @@ private function createProduct( return $product; } + + /** + * @When the customer bought a single bundle :product + */ + public function theCustomerBoughtBundle(ProductInterface $product): void + { + /** @var OrderInterface|null $cart */ + $cart = $this->sharedStorage->get('order'); + /** @var ProductVariantInterface|null $variant */ + $variant = $product->getVariants()->first(); + + $cartItem = $this->cartItemFactory->createWithVariant($variant); + $this->orderItemQuantityModifier->modify($cartItem, 1); + + foreach ($product->getProductBundle()->getProductBundleItems() as $bundleItem) { + $productBundleOrderItem = $this->productBundleOrderItemFactory->createFromProductBundleItem($bundleItem); + $cartItem->addProductBundleOrderItem($productBundleOrderItem); + } + + $this->orderModifier->addToOrder($cart, $cartItem); + + $this->objectManager->flush(); + } + + /** + * @Given product bundle :productBundleCode is packed + */ + public function productBundleIsPacked(string $productBundleCode): void + { + $bundle = $this->productBundleRepository->findOneByProductCode($productBundleCode); + $bundle->setIsPackedProduct(true); + $this->objectManager->flush(); + } } diff --git a/tests/Behat/Context/Ui/ProductBundleContext.php b/tests/Behat/Context/Ui/ProductBundleContext.php index 43058dc5..5087e04d 100644 --- a/tests/Behat/Context/Ui/ProductBundleContext.php +++ b/tests/Behat/Context/Ui/ProductBundleContext.php @@ -12,13 +12,21 @@ namespace Tests\BitBag\SyliusProductBundlePlugin\Behat\Context\Ui; use Behat\Behat\Context\Context; +use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface; +use BitBag\SyliusProductBundlePlugin\Entity\ProductInterface; use Sylius\Component\Core\Model\ChannelInterface; +use Sylius\Component\Core\Repository\ProductRepositoryInterface; use Tests\BitBag\SyliusProductBundlePlugin\Behat\Page\Admin\CreateBundledProductPageInterface; +use Tests\BitBag\SyliusProductBundlePlugin\Behat\Page\Shop\BundledProductsListPageInterface; +use Webmozart\Assert\Assert; class ProductBundleContext implements Context { public function __construct( private CreateBundledProductPageInterface $createBundledProductPage, + private ProductRepositoryInterface $productRepository, + private BundledProductsListPageInterface $summaryPage, + private BundledProductsListPageInterface $orderShowPage, ) { } @@ -90,4 +98,67 @@ public function iAddProductsToBundledProduct(...$productsNames) { $this->createBundledProductPage->addProductsToBundle($productsNames); } + + /** + * @When I add product :firstProductName with quantity :firstQuantity and :secondProductName with quantity :secondQuantity to the bundle + */ + public function iAddProductsWithQuantitiesToBundledProduct(string $firstProductName, string $secondProductName, int $firstQuantity, int $secondQuantity) + { + $this->createBundledProductPage->addProductsToBundle([$firstProductName, $secondProductName], [$firstQuantity, $secondQuantity]); + } + + /** + * @When there should be a :bundleName bundle containing :productName with quantity :quantity + */ + public function theProductBundleShouldContainProductWithQuantity(string $bundleCode, string $productName, int $quantity): void + { + /** @var ProductInterface $product */ + $product = $this->productRepository->findOneBy(['code' => $bundleCode]); + + Assert::notNull($product->getProductBundle()); + + $bundleItems = $product->getProductBundle()->getProductBundleItems(); + /** @var ProductBundleItemInterface $item */ + foreach ($bundleItems as $item) { + if ($item->getProductVariant()->getProduct()->getName() === $productName) { + Assert::same($item->getQuantity(), $quantity); + + return; + } + } + + throw new \InvalidArgumentException(sprintf('Product "%s" not found in bundle "%s"', $productName, $bundleCode)); + } + + /** + * @When there should be bundled products listed + */ + public function thereShouldBeBundledProductsListed(): void + { + Assert::true($this->summaryPage->hasBundledProductsList()); + } + + /** + * @When the list should contain :productName + */ + public function thereShouldBeAProductOnTheList(string $productName): void + { + Assert::true($this->summaryPage->hasBundledProduct($productName)); + } + + /** + * @When there should be bundled products listed in order details + */ + public function thereShouldBeBundledProductsListedOnOrderShowPage(): void + { + Assert::true($this->orderShowPage->hasBundledProductsList()); + } + + /** + * @When the list should contain :productName in order details + */ + public function thereShouldBeAProductOnTheListOnOrderShowPage(string $productName): void + { + Assert::true($this->orderShowPage->hasBundledProduct($productName)); + } } diff --git a/tests/Behat/Page/Admin/CreateBundledProductPage.php b/tests/Behat/Page/Admin/CreateBundledProductPage.php index eb2f8ae0..23067825 100644 --- a/tests/Behat/Page/Admin/CreateBundledProductPage.php +++ b/tests/Behat/Page/Admin/CreateBundledProductPage.php @@ -60,13 +60,13 @@ public function specifyOriginalPrice(ChannelInterface $channel, int $originalPri $this->getElement('original_price', ['%channelCode%' => $channel->getCode()])->setValue($originalPrice); } - public function addProductsToBundle(array $productsNames): void + public function addProductsToBundle(array $productsNames, array $quantites = []): void { $this->clickTabIfItsNotActive('bundle'); $productCounter = 0; - foreach ($productsNames as $productName) { + foreach ($productsNames as $i => $productName) { $addSelector = $this->getElement('add_product_to_bundle_button'); $addSelector->click(); $addSelector->waitFor(5, fn () => $this->hasElement('product_selector_dropdown')); @@ -80,7 +80,8 @@ public function addProductsToBundle(array $productsNames): void ]); $item->click(); - $this->getElement('product_selector_quantity', ['%productCounter%' => $productCounter])->setValue('1'); + $quantity = array_key_exists($i, $quantites) ? (string) $quantites[$i] : '1'; + $this->getElement('product_selector_quantity', ['%productCounter%' => $productCounter])->setValue($quantity); ++$productCounter; } diff --git a/tests/Behat/Page/Shop/BundledProductsListPageInterface.php b/tests/Behat/Page/Shop/BundledProductsListPageInterface.php new file mode 100644 index 00000000..971d19aa --- /dev/null +++ b/tests/Behat/Page/Shop/BundledProductsListPageInterface.php @@ -0,0 +1,19 @@ +hasElement('products_in_bundle'); + } + + public function hasBundledProduct(string $productName): bool + { + return $this->hasElement('bundled_product', ['%productName%' => $productName]); + } + + protected function getDefinedElements(): array + { + return array_merge(parent::getDefinedElements(), [ + 'products_in_bundle' => '#sylius-order > tbody > tr:nth-child(2) > td > div > div.title.bundled-items-header > strong:contains("Products in bundle")', + 'bundled_product' => '#sylius-order > tbody > tr:nth-child(2) > td > div > div.content.bundled-items > div > div > div > div:contains("%productName%")', + ]); + } +} diff --git a/tests/Behat/Page/Shop/SummaryPage.php b/tests/Behat/Page/Shop/SummaryPage.php new file mode 100644 index 00000000..8acb33fd --- /dev/null +++ b/tests/Behat/Page/Shop/SummaryPage.php @@ -0,0 +1,35 @@ +hasElement('products_in_bundle'); + } + + public function hasBundledProduct(string $productName): bool + { + return $this->hasElement('bundled_product', ['%productName%' => $productName]); + } + + protected function getDefinedElements(): array + { + return array_merge(parent::getDefinedElements(), [ + 'products_in_bundle' => '#sylius-cart-items > tbody > tr:nth-child(2) > td > div > div.title.bundled-items-header > strong:contains("Products in bundle")', + 'bundled_product' => '#sylius-cart-items > tbody > tr:nth-child(2) > td > div > div.content.bundled-items > div > div > div > div:contains("%productName%")', + ]); + } +} diff --git a/tests/Behat/Resources/services.yml b/tests/Behat/Resources/services.yml index f6ae41e7..25ae9378 100644 --- a/tests/Behat/Resources/services.yml +++ b/tests/Behat/Resources/services.yml @@ -14,6 +14,12 @@ services: - '@sylius.factory.channel_pricing' - '@sylius.product_variant_resolver.default' - '@sylius.generator.slug' + - '@doctrine.orm.entity_manager' + - '@sylius.order_item_quantity_modifier' + - '@bitbag_sylius_product_bundle.custom_factory.product_bundle_order_item' + - '@sylius.order_modifier' + - '@bitbag_sylius_product_bundle.custom_factory.order_item' + - '@bitbag_sylius_product_bundle.repository.product_bundle' bitbag_sylius_product_bundle_plugin.behat.page.create_bundled_product_page: class: Tests\BitBag\SyliusProductBundlePlugin\Behat\Page\Admin\CreateBundledProductPage @@ -22,7 +28,29 @@ services: arguments: - 'bitbag_product_bundle_admin_product_create_bundle' + bitbag_sylius_product_bundle_plugin.behat.page.summary_page: + class: Tests\BitBag\SyliusProductBundlePlugin\Behat\Page\Shop\SummaryPage + parent: sylius.behat.page.shop.cart_summary + public: false + + bitbag_sylius_product_bundle_plugin.behat.page.account.order_show_page: + class: Tests\BitBag\SyliusProductBundlePlugin\Behat\Page\Shop\OrderShowPage + parent: sylius.behat.page.shop.account.order.show + public: false + bitbag_sylius_product_bundle_plugin.behat.context.ui.product_bundle: class: Tests\BitBag\SyliusProductBundlePlugin\Behat\Context\Ui\ProductBundleContext arguments: - '@bitbag_sylius_product_bundle_plugin.behat.page.create_bundled_product_page' + - '@sylius.repository.product' + - '@bitbag_sylius_product_bundle_plugin.behat.page.summary_page' + - '@bitbag_sylius_product_bundle_plugin.behat.page.account.order_show_page' + + bitbag_sylius_product_bundle_plugin.behat.context.api.product_bundle: + class: Tests\BitBag\SyliusProductBundlePlugin\Behat\Context\Api\ProductBundleContext + arguments: + - '@sylius.behat.shared_storage' + - '@sylius.behat.api_platform_client.shop' + - '@sylius.behat.request_factory' + - '@Sylius\Behat\Client\ResponseCheckerInterface' + - '@sylius.repository.product_variant' diff --git a/tests/Behat/Resources/suites.yml b/tests/Behat/Resources/suites.yml index 70c3f6c8..56af2b95 100644 --- a/tests/Behat/Resources/suites.yml +++ b/tests/Behat/Resources/suites.yml @@ -4,24 +4,29 @@ default: contexts: - sylius.behat.context.hook.doctrine_orm - - sylius.behat.context.transform.lexical - - sylius.behat.context.transform.product - - sylius.behat.context.transform.address - - sylius.behat.context.transform.payment - - sylius.behat.context.transform.shipping_method - - sylius.behat.context.transform.zone - - sylius.behat.context.transform.locale - - sylius.behat.context.transform.channel - - - sylius.behat.context.setup.currency - - sylius.behat.context.setup.locale + - sylius.behat.context.setup.admin_security - sylius.behat.context.setup.channel + - sylius.behat.context.setup.currency - sylius.behat.context.setup.customer - - sylius.behat.context.setup.shop_security + - sylius.behat.context.setup.locale + - sylius.behat.context.setup.payment - sylius.behat.context.setup.product - - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.promotion + - sylius.behat.context.setup.taxation - sylius.behat.context.setup.shipping - - sylius.behat.context.setup.payment + - sylius.behat.context.setup.shop_security + + - sylius.behat.context.transform.address + - sylius.behat.context.transform.channel + - 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.tax_category + - sylius.behat.context.transform.zone - sylius.behat.context.ui.shop.cart - sylius.behat.context.ui.shop.checkout.addressing @@ -30,7 +35,75 @@ default: - sylius.behat.context.ui.shop.checkout.thank_you - sylius.behat.context.ui.admin.notification + - bitbag_sylius_product_bundle_plugin.behat.context.ui.product_bundle - bitbag_sylius_product_bundle_plugin.behat.context.setup.product_bundle + + filters: + tags: "@bundled_product&&@ui&&~@shop" + + shop_bundled_product: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.customer + - 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_security + + - sylius.behat.context.transform.address + - 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.shared_storage + - sylius.behat.context.transform.shipping_method + + - sylius.behat.context.ui.shop.account + - sylius.behat.context.ui.shop.cart + - bitbag_sylius_product_bundle_plugin.behat.context.ui.product_bundle + - bitbag_sylius_product_bundle_plugin.behat.context.setup.product_bundle + + filters: + tags: "@bundled_product&&@ui&&@shop" + + api_bundled_product: + contexts: + - sylius.behat.context.hook.doctrine_orm + + - sylius.behat.context.setup.admin_security + - sylius.behat.context.setup.channel + - sylius.behat.context.setup.currency + - sylius.behat.context.setup.customer + - sylius.behat.context.setup.locale + - sylius.behat.context.setup.payment + - sylius.behat.context.setup.product + - sylius.behat.context.setup.promotion + - sylius.behat.context.setup.taxation + - sylius.behat.context.setup.shipping + - sylius.behat.context.setup.shop_security + + - sylius.behat.context.transform.address + - sylius.behat.context.transform.channel + - 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.tax_category + - sylius.behat.context.transform.zone + + - sylius.behat.context.api.shop.cart + - sylius.behat.context.api.shop.checkout + + - bitbag_sylius_product_bundle_plugin.behat.context.api.product_bundle + - bitbag_sylius_product_bundle_plugin.behat.context.setup.product_bundle + filters: - tags: "@bundled_product" + tags: "@bundled_product&&@api" diff --git a/tests/Unit/DataTransformer/AddProductBundleToCartDtoDataTransformerTest.php b/tests/Unit/DataTransformer/AddProductBundleToCartDtoDataTransformerTest.php index f4ec9ad8..a7fc87ec 100644 --- a/tests/Unit/DataTransformer/AddProductBundleToCartDtoDataTransformerTest.php +++ b/tests/Unit/DataTransformer/AddProductBundleToCartDtoDataTransformerTest.php @@ -7,19 +7,17 @@ * an email on hello@bitbag.io. */ -/* - * This file was created by developers working at BitBag - * Do you need more information about us and what we do? Visit our https://bitbag.io website! - * We are hiring developers from all over the world. Join us and start your new, exciting adventure and become part of us: https://bitbag.io/career - */ - declare(strict_types=1); namespace Tests\BitBag\SyliusProductBundlePlugin\Unit\DataTransformer; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleToCartCommand; use BitBag\SyliusProductBundlePlugin\DataTransformer\AddProductBundleToCartDtoDataTransformer; use BitBag\SyliusProductBundlePlugin\Dto\Api\AddProductBundleToCartDto; +use BitBag\SyliusProductBundlePlugin\Provider\AddProductBundleItemToCartCommandProviderInterface; +use Doctrine\Common\Collections\ArrayCollection; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Tests\BitBag\SyliusProductBundlePlugin\Unit\MotherObject\Api\AddProductBundleToCartDtoMother; use Tests\BitBag\SyliusProductBundlePlugin\Unit\MotherObject\OrderMother; @@ -28,6 +26,16 @@ final class AddProductBundleToCartDtoDataTransformerTest extends TestCase { + private AddProductBundleItemToCartCommandProviderInterface|MockObject $provider; + + private AddProductBundleItemToCartCommandInterface|MockObject $addProductBundleItemToCartCommand; + + public function setUp(): void + { + $this->provider = $this->createMock(AddProductBundleItemToCartCommandProviderInterface::class); + $this->addProductBundleItemToCartCommand = $this->createMock(AddProductBundleItemToCartCommandInterface::class); + } + public function testThrowErrorIfObjectIsNotInstanceOfAddProductBundleToCartDto(): void { $this->expectException(InvalidArgumentException::class); @@ -36,7 +44,8 @@ public function testThrowErrorIfObjectIsNotInstanceOfAddProductBundleToCartDto() ); $object = new \stdClass(); - $dataTransformer = new AddProductBundleToCartDtoDataTransformer(); + $this->provider->expects(self::never())->method(self::anything()); + $dataTransformer = new AddProductBundleToCartDtoDataTransformer($this->provider); $dataTransformer->transform($object, ''); } @@ -47,7 +56,8 @@ public function testThrowIfObjectToPopulateDoesntExist(): void $this->expectExceptionMessage(TypeExceptionMessage::EXPECTED_VALUE_OTHER_THAN_NULL); $object = AddProductBundleToCartDtoMother::create('PRODUCT_CODE'); - $dataTransformer = new AddProductBundleToCartDtoDataTransformer(); + $this->provider->expects(self::never())->method(self::anything()); + $dataTransformer = new AddProductBundleToCartDtoDataTransformer($this->provider); $dataTransformer->transform($object, ''); } @@ -58,7 +68,16 @@ public function testReturnAddProductBundleToCart(): void $context = [ AddProductBundleToCartDtoDataTransformer::OBJECT_TO_POPULATE => OrderMother::createWithId(3), ]; - $dataTransformer = new AddProductBundleToCartDtoDataTransformer(); + + $addProductBundleItemToCartCommands = new ArrayCollection([$this->addProductBundleItemToCartCommand]); + + $this->provider + ->expects(self::once()) + ->method('provide') + ->with('PRODUCT_CODE', []) + ->willReturn($addProductBundleItemToCartCommands); + + $dataTransformer = new AddProductBundleToCartDtoDataTransformer($this->provider); $addProductBundleToCartCommand = $dataTransformer->transform($object, '', $context); @@ -66,5 +85,6 @@ public function testReturnAddProductBundleToCart(): void self::assertSame('PRODUCT_CODE', $addProductBundleToCartCommand->getProductCode()); self::assertSame(2, $addProductBundleToCartCommand->getQuantity()); self::assertSame(3, $addProductBundleToCartCommand->getOrderId()); + self::assertSame($addProductBundleItemToCartCommands, $addProductBundleToCartCommand->getProductBundleItems()); } } diff --git a/tests/Unit/EventListener/AddProductToProductBundleWhenEditNormalProductEventListenerTest.php b/tests/Unit/EventListener/AddProductToProductBundleWhenEditNormalProductEventListenerTest.php new file mode 100644 index 00000000..7f8fb7ab --- /dev/null +++ b/tests/Unit/EventListener/AddProductToProductBundleWhenEditNormalProductEventListenerTest.php @@ -0,0 +1,110 @@ +instance = new AddProductToProductBundleWhenEditNormalProductEventListener(); + $this->resourceControllerEvent = $this->createMock(ResourceControllerEvent::class); + $this->product = $this->createMock(ProductInterface::class); + $this->productBundle = $this->createMock(ProductBundleInterface::class); + } + + public function testAddProductToProductBundle(): void + { + $this->productBundle + ->expects(self::once()) + ->method('getProduct') + ->willReturn(null); + + $this->product + ->expects(self::exactly(3)) + ->method('getProductBundle') + ->willReturn($this->productBundle); + + $this->productBundle + ->expects(self::once()) + ->method('setProduct') + ->with($this->product); + + $this->resourceControllerEvent + ->expects(self::once()) + ->method('getSubject') + ->willReturn($this->product); + + $this->instance->addProductToProductBundle($this->resourceControllerEvent); + } + + public function testWillNotAddProductToProductBundleIfProductHasBundle(): void + { + $this->productBundle + ->expects(self::never()) + ->method('getProduct'); + + $this->product + ->expects(self::once()) + ->method('getProductBundle') + ->willReturn(null); + + $this->productBundle + ->expects(self::never()) + ->method('setProduct'); + + $this->resourceControllerEvent + ->expects(self::once()) + ->method('getSubject') + ->willReturn($this->product); + + $this->instance->addProductToProductBundle($this->resourceControllerEvent); + } + + public function testWillNotAddProductToProductBundleIfProductBundleHasProduct(): void + { + $this->productBundle + ->expects(self::once()) + ->method('getProduct') + ->willReturn($this->product); + + $this->product + ->expects(self::exactly(2)) + ->method('getProductBundle') + ->willReturn($this->productBundle); + + $this->productBundle + ->expects(self::never()) + ->method('setProduct'); + + $this->resourceControllerEvent + ->expects(self::once()) + ->method('getSubject') + ->willReturn($this->product); + + $this->instance->addProductToProductBundle($this->resourceControllerEvent); + } +} diff --git a/tests/Unit/Factory/AddProductBundleItemToCartCommandFactoryTest.php b/tests/Unit/Factory/AddProductBundleItemToCartCommandFactoryTest.php index 2a3f0764..e77b6425 100644 --- a/tests/Unit/Factory/AddProductBundleItemToCartCommandFactoryTest.php +++ b/tests/Unit/Factory/AddProductBundleItemToCartCommandFactoryTest.php @@ -11,7 +11,7 @@ namespace Tests\BitBag\SyliusProductBundlePlugin\Unit\Factory; -use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommand; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Factory\AddProductBundleItemToCartCommandFactory; use PHPUnit\Framework\TestCase; use Tests\BitBag\SyliusProductBundlePlugin\Unit\MotherObject\ProductBundleItemMother; @@ -25,6 +25,6 @@ public function testCreateAddProductBundleItemToCartCommand(): void $factory = new AddProductBundleItemToCartCommandFactory(); $command = $factory->createNew($productBundleItem); - self::assertInstanceOf(AddProductBundleItemToCartCommand::class, $command); + self::assertInstanceOf(AddProductBundleItemToCartCommandInterface::class, $command); } } diff --git a/tests/Unit/Factory/AddProductBundleToCartCommandFactoryTest.php b/tests/Unit/Factory/AddProductBundleToCartCommandFactoryTest.php index 4df89fd4..7e4c0bb3 100644 --- a/tests/Unit/Factory/AddProductBundleToCartCommandFactoryTest.php +++ b/tests/Unit/Factory/AddProductBundleToCartCommandFactoryTest.php @@ -11,8 +11,10 @@ namespace Tests\BitBag\SyliusProductBundlePlugin\Unit\Factory; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleToCartCommand; use BitBag\SyliusProductBundlePlugin\Factory\AddProductBundleToCartCommandFactory; +use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\TestCase; use Tests\BitBag\SyliusProductBundlePlugin\Unit\MotherObject\AddProductBundleToCartDtoMother; @@ -26,13 +28,17 @@ final class AddProductBundleToCartCommandFactoryTest extends TestCase public function testCreateAddProductBundleToCartCommandObject(): void { + $addProductBundleItemToCartCommand = $this->createMock(AddProductBundleItemToCartCommandInterface::class); + $commands = new ArrayCollection([$addProductBundleItemToCartCommand]); + $factory = new AddProductBundleToCartCommandFactory(); - $command = $factory->createNew(self::ORDER_ID, self::PRODUCT_CODE, self::QUANTITY); + $command = $factory->createNew(self::ORDER_ID, self::PRODUCT_CODE, self::QUANTITY, $commands); self::assertInstanceOf(AddProductBundleToCartCommand::class, $command); self::assertEquals(self::ORDER_ID, $command->getOrderId()); self::assertEquals(self::PRODUCT_CODE, $command->getProductCode()); self::assertEquals(self::QUANTITY, $command->getQuantity()); + self::assertEquals($commands, $command->getProductBundleItems()); } public function testCreateAddProductBundleToCartCommandObjectFromDto(): void diff --git a/tests/Unit/Handler/AddProductBundleToCartHandler/CartProcessorTest.php b/tests/Unit/Handler/AddProductBundleToCartHandler/CartProcessorTest.php index 3a492db4..64bea086 100644 --- a/tests/Unit/Handler/AddProductBundleToCartHandler/CartProcessorTest.php +++ b/tests/Unit/Handler/AddProductBundleToCartHandler/CartProcessorTest.php @@ -11,11 +11,10 @@ namespace Tests\BitBag\SyliusProductBundlePlugin\Unit\Handler\AddProductBundleToCartHandler; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Entity\OrderItemInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundle; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleInterface; -use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItem; -use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleOrderItem; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleOrderItemInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductInterface; @@ -23,6 +22,7 @@ use BitBag\SyliusProductBundlePlugin\Factory\ProductBundleOrderItemFactoryInterface; use BitBag\SyliusProductBundlePlugin\Handler\AddProductBundleToCartHandler\CartProcessor; use BitBag\SyliusProductBundlePlugin\Handler\AddProductBundleToCartHandler\CartProcessorInterface; +use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Sylius\Component\Core\Model\ProductVariant; @@ -37,17 +37,15 @@ final class CartProcessorTest extends TestCase { - /** @var mixed|MockObject|OrderItemQuantityModifierInterface */ - private $orderItemQuantityModifier; + private OrderItemQuantityModifierInterface|MockObject $orderItemQuantityModifier; - /** @var ProductBundleOrderItemFactoryInterface|mixed|MockObject */ - private $productBundleOrderItemFactory; + private ProductBundleOrderItemFactoryInterface|MockObject $productBundleOrderItemFactory; - /** @var mixed|MockObject|OrderModifierInterface */ - private $orderModifier; + private OrderModifierInterface|MockObject $orderModifier; - /** @var OrderItemFactoryInterface|mixed|MockObject */ - private $cartItemFactory; + private OrderItemFactoryInterface|MockObject $cartItemFactory; + + private AddProductBundleItemToCartCommandInterface|MockObject $addProductBundleItemToCartCommand; protected function setUp(): void { @@ -55,6 +53,7 @@ protected function setUp(): void $this->productBundleOrderItemFactory = $this->createMock(ProductBundleOrderItemFactoryInterface::class); $this->orderModifier = $this->createMock(OrderModifierInterface::class); $this->cartItemFactory = $this->createMock(OrderItemFactoryInterface::class); + $this->addProductBundleItemToCartCommand = $this->createMock(AddProductBundleItemToCartCommandInterface::class); } public function testThrowExceptionIfQuantityNotGreaterThanZero(): void @@ -65,7 +64,7 @@ public function testThrowExceptionIfQuantityNotGreaterThanZero(): void $productBundle = $this->createProductBundle(); $processor = $this->createProcessor(); - $processor->process($cart, $productBundle, 0); + $processor->process($cart, $productBundle, 0, new ArrayCollection()); } public function testThrowExceptionIfProductIsNull(): void @@ -76,7 +75,7 @@ public function testThrowExceptionIfProductIsNull(): void $productBundle = $this->createProductBundle(); $processor = $this->createProcessor(); - $processor->process($cart, $productBundle, 1); + $processor->process($cart, $productBundle, 1, new ArrayCollection()); } public function testThrowExceptionIfProductHasNoVariant(): void @@ -87,7 +86,7 @@ public function testThrowExceptionIfProductHasNoVariant(): void $productBundle = $this->createProductBundleWithProduct(); $processor = $this->createProcessor(); - $processor->process($cart, $productBundle, 1); + $processor->process($cart, $productBundle, 1, new ArrayCollection()); } public function testCreateCartItem(): void @@ -103,7 +102,7 @@ public function testCreateCartItem(): void ; $processor = $this->createProcessor(); - $processor->process($cart, $productBundle, 2); + $processor->process($cart, $productBundle, 2, new ArrayCollection([$this->addProductBundleItemToCartCommand])); } public function testModifyCartItemQuantity(): void @@ -124,13 +123,13 @@ public function testModifyCartItemQuantity(): void ; $processor = $this->createProcessor(); - $processor->process($cart, $productBundle, 2); + $processor->process($cart, $productBundle, 2, new ArrayCollection([$this->addProductBundleItemToCartCommand])); } public function testCreateBundleOrderItemsFromBundleItems(): void { - $bundleItem1 = $this->createProductBundleItem(); - $bundleItem2 = $this->createProductBundleItem(); + $addBundleItemToCartCommand1 = $this->createMock(AddProductBundleItemToCartCommandInterface::class); + $addBundleItemToCartCommand2 = $this->createMock(AddProductBundleItemToCartCommandInterface::class); $productBundleOrderItem1 = $this->createProductBundleOrderItem(); $productBundleOrderItem2 = $this->createProductBundleOrderItem(); @@ -138,8 +137,6 @@ public function testCreateBundleOrderItemsFromBundleItems(): void $cart = $this->createCart(); $product = $this->createProductWithVariant(); $productBundle = $this->createProductBundleWithProduct($product); - $productBundle->addProductBundleItem($bundleItem1); - $productBundle->addProductBundleItem($bundleItem2); $cartItem = $this->createMock(OrderItemInterface::class); $cartItem->expects(self::exactly(2)) @@ -152,13 +149,13 @@ public function testCreateBundleOrderItemsFromBundleItems(): void ->willReturn($cartItem) ; $this->productBundleOrderItemFactory->expects(self::exactly(2)) - ->method('createFromProductBundleItem') - ->withConsecutive([$bundleItem1], [$bundleItem2]) + ->method('createFromAddProductBundleItemToCartCommand') + ->withConsecutive([$addBundleItemToCartCommand1], [$addBundleItemToCartCommand2]) ->willReturn($productBundleOrderItem1, $productBundleOrderItem2) ; $processor = $this->createProcessor(); - $processor->process($cart, $productBundle, 1); + $processor->process($cart, $productBundle, 1, new ArrayCollection([$addBundleItemToCartCommand1, $addBundleItemToCartCommand2])); } public function testAddCartItemToOrder(): void @@ -178,7 +175,7 @@ public function testAddCartItemToOrder(): void ; $processor = $this->createProcessor(); - $processor->process($cart, $productBundle, 1); + $processor->process($cart, $productBundle, 1, new ArrayCollection([$this->addProductBundleItemToCartCommand])); } private function createProcessor(): CartProcessorInterface @@ -230,11 +227,6 @@ private function createProductBundleWithProduct(?ProductInterface $product = nul return $productBundle; } - private function createProductBundleItem(): ProductBundleItemInterface - { - return new ProductBundleItem(); - } - private function createProductBundleOrderItem(): ProductBundleOrderItemInterface { return new ProductBundleOrderItem(); diff --git a/tests/Unit/Handler/AddProductBundleToCartHandlerTest.php b/tests/Unit/Handler/AddProductBundleToCartHandlerTest.php index 922efa27..4df1d108 100644 --- a/tests/Unit/Handler/AddProductBundleToCartHandlerTest.php +++ b/tests/Unit/Handler/AddProductBundleToCartHandlerTest.php @@ -11,10 +11,12 @@ namespace Tests\BitBag\SyliusProductBundlePlugin\Unit\Handler; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleToCartCommand; use BitBag\SyliusProductBundlePlugin\Entity\ProductInterface; use BitBag\SyliusProductBundlePlugin\Handler\AddProductBundleToCartHandler; use BitBag\SyliusProductBundlePlugin\Handler\AddProductBundleToCartHandler\CartProcessorInterface; +use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Sylius\Component\Core\Model\OrderInterface; @@ -28,20 +30,20 @@ final class AddProductBundleToCartHandlerTest extends TestCase { - /** @var mixed|MockObject|OrderRepositoryInterface */ - private $orderRepository; + private OrderRepositoryInterface|MockObject $orderRepository; - /** @var mixed|MockObject|ProductRepositoryInterface */ - private $productRepository; + private ProductRepositoryInterface|MockObject $productRepository; - /** @var CartProcessorInterface|mixed|MockObject */ - private $cartProcessor; + private CartProcessorInterface|MockObject $cartProcessor; + + private AddProductBundleItemToCartCommandInterface|MockObject $addProductBundleItemToCartCommand; protected function setUp(): void { $this->orderRepository = $this->createMock(OrderRepositoryInterface::class); $this->productRepository = $this->createMock(ProductRepositoryInterface::class); $this->cartProcessor = $this->createMock(CartProcessorInterface::class); + $this->addProductBundleItemToCartCommand = $this->createMock(AddProductBundleItemToCartCommandInterface::class); } public function testThrowExceptionIfCartDoesntExist(): void @@ -122,10 +124,12 @@ public function testProcessCart(): void $this->cartProcessor->expects(self::once()) ->method('process') - ->with($cart, $productBundle, 2) + ->with($cart, $productBundle, 2, new ArrayCollection([$this->addProductBundleItemToCartCommand])) ; $command = new AddProductBundleToCartCommand(1, '', 2); + $command->setProductBundleItems(new ArrayCollection([$this->addProductBundleItemToCartCommand])); + $handler = $this->createHandler(); $handler($command); } @@ -149,6 +153,8 @@ public function testAddCartToRepository(): void ; $command = new AddProductBundleToCartCommand(1, '', 1); + $command->setProductBundleItems(new ArrayCollection([$this->addProductBundleItemToCartCommand])); + $handler = $this->createHandler(); $handler($command); } diff --git a/tests/Unit/Inventory/Checker/BundledProductsInventoryManagementFeatureFlagCheckerTest.php b/tests/Unit/Inventory/Checker/BundledProductsInventoryManagementFeatureFlagCheckerTest.php new file mode 100644 index 00000000..2478a637 --- /dev/null +++ b/tests/Unit/Inventory/Checker/BundledProductsInventoryManagementFeatureFlagCheckerTest.php @@ -0,0 +1,30 @@ +isEnabled()); + } + + public function testIsDisabled(): void + { + $checker = new BundledProductsInventoryManagementFeatureFlagChecker(false); + self::assertFalse($checker->isEnabled()); + } +} diff --git a/tests/Unit/Inventory/Checker/OrderItemAvailabilityCheckerTest.php b/tests/Unit/Inventory/Checker/OrderItemAvailabilityCheckerTest.php new file mode 100644 index 00000000..54d64fe2 --- /dev/null +++ b/tests/Unit/Inventory/Checker/OrderItemAvailabilityCheckerTest.php @@ -0,0 +1,117 @@ +decorated = $this->createMock(OrderItemAvailabilityCheckerInterface::class); + $this->featureFlagChecker = $this->createMock(FeatureFlagCheckerInterface::class); + $this->bundleOrderItemAvailabilityChecker = $this->createMock(ProductBundleOrderItemAvailabilityCheckerInterface::class); + + $this->checker = new OrderItemAvailabilityChecker( + $this->decorated, + $this->featureFlagChecker, + $this->bundleOrderItemAvailabilityChecker, + ); + } + + public function testItCallsDecoratedIfFeatureDisabled(): void + { + $this->featureFlagChecker + ->expects(self::once()) + ->method('isEnabled') + ->willReturn(false); + + $this->decorated + ->expects(self::once()) + ->method('isReservedStockSufficient'); + + $this->bundleOrderItemAvailabilityChecker->expects(self::never())->method(self::anything()); + + $this->checker->isReservedStockSufficient($this->createMock(OrderItemInterface::class)); + } + + public function testItCallsDecoratedIfProductIsNotBundle(): void + { + $this->featureFlagChecker + ->expects(self::once()) + ->method('isEnabled') + ->willReturn(true); + $this->decorated + ->expects(self::once()) + ->method('isReservedStockSufficient'); + + $this->bundleOrderItemAvailabilityChecker->expects(self::never())->method(self::anything()); + + $product = $this->createMock(ProductInterface::class); + $product + ->expects(self::once()) + ->method('isBundle') + ->willReturn(false); + + $orderItem = $this->createMock(OrderItemInterface::class); + $orderItem + ->expects(self::once()) + ->method('getProduct') + ->willReturn($product); + + $this->checker->isReservedStockSufficient($orderItem); + } + + public function testItCallsProductBundleCheckerIfProductIsBundle(): void + { + $this->featureFlagChecker + ->expects(self::once()) + ->method('isEnabled') + ->willReturn(true); + + $this->decorated->expects(self::never())->method(self::anything()); + + $product = $this->createMock(ProductInterface::class); + $product + ->expects(self::once()) + ->method('isBundle') + ->willReturn(true); + + $orderItem = $this->createMock(OrderItemInterface::class); + $orderItem + ->expects(self::once()) + ->method('getProduct') + ->willReturn($product); + + $this->bundleOrderItemAvailabilityChecker + ->expects(self::once()) + ->method('areOrderedBundledProductVariantsAvailable') + ->with($orderItem); + + $this->checker->isReservedStockSufficient($orderItem); + } +} diff --git a/tests/Unit/Inventory/Checker/ProductBundleOrderItemAvailabilityCheckerTest.php b/tests/Unit/Inventory/Checker/ProductBundleOrderItemAvailabilityCheckerTest.php new file mode 100644 index 00000000..91402120 --- /dev/null +++ b/tests/Unit/Inventory/Checker/ProductBundleOrderItemAvailabilityCheckerTest.php @@ -0,0 +1,133 @@ +areOrderedBundledProductVariantsAvailable($orderItem)); + } + + public function provideAreOrderedBundledProductVariantsAvailable(): array + { + $unTrackedVariant = $this->mockProductVariant(false, 0, 0); + $unTrackedBundleOrderItem = $this->mockBundleOrderItem($unTrackedVariant, 2); + $orderItem1 = $this->mockOrderItem([$unTrackedBundleOrderItem], 2); + + $trackedVariantOutOfStock = $this->mockProductVariant(true, 0, 0); + $trackedOutOfStockBundleOrderItem = $this->mockBundleOrderItem($trackedVariantOutOfStock, 2); + $orderItem2 = $this->mockOrderItem([$trackedOutOfStockBundleOrderItem], 2); + + $trackedVariantInStock = $this->mockProductVariant(true, 10, 20); + $trackedInStockBundleOrderItem = $this->mockBundleOrderItem($trackedVariantInStock, 2); + $orderItem3 = $this->mockOrderItem([$trackedOutOfStockBundleOrderItem, $trackedInStockBundleOrderItem], 2); + + $orderItem4 = $this->mockOrderItem([$trackedInStockBundleOrderItem], 5); + + $trackedVariantInStock2 = $this->mockProductVariant(true, 20, 10); + $trackedInStockBundleOrderItem2 = $this->mockBundleOrderItem($trackedVariantInStock2, 2); + $orderItem5 = $this->mockOrderItem([$trackedInStockBundleOrderItem2], 5); + + $trackedVariantInStock3 = $this->mockProductVariant(true, 10, 9); + $trackedInStockBundleOrderItem3 = $this->mockBundleOrderItem($trackedVariantInStock3, 2); + $orderItem6 = $this->mockOrderItem([$trackedInStockBundleOrderItem3], 5); + + $trackedVariantInStock4 = $this->mockProductVariant(true, 9, 10); + $trackedInStockBundleOrderItem4 = $this->mockBundleOrderItem($trackedVariantInStock4, 2); + $orderItem7 = $this->mockOrderItem([$trackedInStockBundleOrderItem4], 5); + + return [ + 'untracked variant' => [ + $orderItem1, + true, + ], + 'variant out of stock' => [ + $orderItem2, + false, + ], + 'one variant out of stock, one in stock' => [ + $orderItem3, + false, + ], + 'on-hold edge case' => [ + $orderItem4, + true, + ], + 'on-hand edge case' => [ + $orderItem5, + true, + ], + 'on-hold insufficient' => [ + $orderItem6, + false, + ], + 'on-hand insufficient' => [ + $orderItem6, + false, + ], + ]; + } + + private function mockProductVariant(bool $isTracked, int $onHold, int $onHand): ProductVariantInterface + { + $variant = $this->createMock(ProductVariantInterface::class); + $variant + ->method('isTracked') + ->willReturn($isTracked); + $variant + ->method('getOnHold') + ->willReturn($onHold); + $variant + ->method('getOnHand') + ->willReturn($onHand); + + return $variant; + } + + private function mockBundleOrderItem(ProductVariantInterface $variant, int $quantity): ProductBundleOrderItemInterface + { + $item = $this->createMock(ProductBundleOrderItemInterface::class); + $item + ->method('getProductVariant') + ->willReturn($variant); + $item + ->method('getQuantity') + ->willReturn($quantity); + + return $item; + } + + /** @param ProductBundleOrderItemInterface[] $bundleOrderItems */ + private function mockOrderItem(array $bundleOrderItems, int $quantity): OrderItemInterface + { + $orderItem = $this->createMock(OrderItemInterface::class); + $orderItem + ->method('getProductBundleOrderItems') + ->willReturn($bundleOrderItems); + $orderItem + ->method('getQuantity') + ->willReturn($quantity); + + return $orderItem; + } +} diff --git a/tests/Unit/Inventory/Operator/OrderInventoryOperatorTest.php b/tests/Unit/Inventory/Operator/OrderInventoryOperatorTest.php new file mode 100644 index 00000000..a19b01be --- /dev/null +++ b/tests/Unit/Inventory/Operator/OrderInventoryOperatorTest.php @@ -0,0 +1,175 @@ +decorated = $this->createMock(SyliusOrderInventoryOperatorInterface::class); + $this->productVariantManager = $this->createMock(EntityManagerInterface::class); + $this->featureFlagChecker = $this->createMock(FeatureFlagCheckerInterface::class); + $this->productBundleOrderInventoryOperator = $this->createMock(ProductBundleOrderInventoryOperatorInterface::class); + + $this->orderInventoryOperator = new OrderInventoryOperator( + $this->decorated, + $this->productVariantManager, + $this->featureFlagChecker, + $this->productBundleOrderInventoryOperator, + ); + } + + public function testCancelCallsDecoratedIfFeatureFlagDisabled(): void + { + $order = $this->createMock(OrderInterface::class); + + $this->featureFlagChecker + ->expects(self::once()) + ->method('isEnabled') + ->willReturn(false); + + $this->decorated + ->expects(self::once()) + ->method('cancel') + ->with($order); + + $this->orderInventoryOperator->cancel($order); + } + + public function testCancelGivesBackInventoryIfOrderPaidOrRefunded(): void + { + $order = $this->createMock(OrderInterface::class); + + $order->method('getPaymentState') + ->willReturn(OrderPaymentStates::STATE_PAID); + + $this->featureFlagChecker + ->method('isEnabled') + ->willReturn(true); + + $this->productBundleOrderInventoryOperator + ->expects(self::once()) + ->method('giveBack') + ->with($order); + + $this->orderInventoryOperator->cancel($order); + } + + public function testCancelReleasesInventoryIfNotPaidOrRefunded(): void + { + $order = $this->createMock(OrderInterface::class); + + $order->method('getPaymentState') + ->willReturn(OrderPaymentStates::STATE_CART); + + $this->featureFlagChecker + ->method('isEnabled') + ->willReturn(true); + + $this->productBundleOrderInventoryOperator + ->expects(self::once()) + ->method('release') + ->with($order); + + $this->orderInventoryOperator->cancel($order); + } + + public function testHoldCallsDecoratedIfFeatureFlagDisabled(): void + { + $order = $this->createMock(OrderInterface::class); + + $this->featureFlagChecker + ->expects(self::once()) + ->method('isEnabled') + ->willReturn(false); + + $this->decorated + ->expects(self::once()) + ->method('hold') + ->with($order); + + $this->orderInventoryOperator->hold($order); + } + + public function testHoldHandlesBundleIfFeatureFlagEnabled(): void + { + $order = $this->createMock(OrderInterface::class); + + $this->decorated->expects(self::never())->method(self::anything()); + + $this->featureFlagChecker + ->method('isEnabled') + ->willReturn(true); + + $this->productBundleOrderInventoryOperator + ->expects(self::once()) + ->method('hold') + ->with($order); + + $this->orderInventoryOperator->hold($order); + } + + public function testSellCallsDecoratedIfFeatureFlagDisabled(): void + { + $order = $this->createMock(OrderInterface::class); + + $this->featureFlagChecker + ->expects(self::once()) + ->method('isEnabled') + ->willReturn(false); + + $this->decorated + ->expects(self::once()) + ->method('sell') + ->with($order); + + $this->orderInventoryOperator->sell($order); + } + + public function testSellHandlesBundleIfFeatureFlagEnabled(): void + { + $order = $this->createMock(OrderInterface::class); + + $this->decorated->expects(self::never())->method(self::anything()); + + $this->featureFlagChecker + ->method('isEnabled') + ->willReturn(true); + + $this->productBundleOrderInventoryOperator + ->expects(self::once()) + ->method('sell') + ->with($order); + + $this->orderInventoryOperator->sell($order); + } +} diff --git a/tests/Unit/Inventory/Operator/ProductBundleOrderInventoryOperatorTest.php b/tests/Unit/Inventory/Operator/ProductBundleOrderInventoryOperatorTest.php new file mode 100644 index 00000000..e6e04a54 --- /dev/null +++ b/tests/Unit/Inventory/Operator/ProductBundleOrderInventoryOperatorTest.php @@ -0,0 +1,368 @@ +bundle = $this->createMock(ProductInterface::class); + $this->bundle + ->method('isBundle') + ->willReturn(true); + + $this->regularProduct = $this->createMock(ProductInterface::class); + $this->regularProduct + ->method('isBundle') + ->willReturn(false); + + $this->operator = new ProductBundleOrderInventoryOperator(); + } + + public function testItHolds(): void + { + $bundleVariant1 = $this->createMock(ProductVariantInterface::class); + $bundleVariant1 + ->method('isTracked') + ->willReturn(true); + $bundleVariant1 + ->method('getOnHold') + ->willReturn(10); + $bundleVariant1 + ->method('setOnHold') + ->with(14); + + $bundleVariant2 = $this->createMock(ProductVariantInterface::class); + $bundleVariant2 + ->method('isTracked') + ->willReturn(true); + $bundleVariant2 + ->method('getOnHold') + ->willReturn(20); + $bundleVariant2 + ->method('setOnHold') + ->with(26); + + $bundleOrderItem = $this->createMock(OrderItemInterface::class); + $bundleOrderItem + ->expects(self::once()) + ->method('getProduct') + ->willReturn($this->bundle); + + $bundleOrderItem + ->expects(self::exactly(2)) + ->method('getQuantity') + ->willReturn(2); + + $bundleOrderItem + ->expects(self::once()) + ->method('getProductBundleOrderItems') + ->willReturn([ + $this->mockProductBundleOrderItem($bundleVariant1, 2), + $this->mockProductBundleOrderItem($bundleVariant2, 3), + ]); + $bundleOrderItem->expects(self::never())->method('getVariant'); + + $regularOrderItem = $this->createMock(OrderItemInterface::class); + $regularOrderItem + ->expects(self::once()) + ->method('getProduct') + ->willReturn($this->regularProduct); + $regularOrderItem + ->expects(self::once()) + ->method('getQuantity') + ->willReturn(3); + + $variant = $this->createMock(ProductVariantInterface::class); + $variant + ->method('isTracked') + ->willReturn(true); + $variant + ->method('getOnHold') + ->willReturn(100); + $variant + ->method('setOnHold') + ->with(103); + + $regularOrderItem + ->expects(self::once()) + ->method('getVariant') + ->willReturn($variant); + + $regularOrderItem->expects(self::never())->method('getProductBundleOrderItems'); + + $order = $this->mockOrder( + $regularOrderItem, + $bundleOrderItem, + ); + + $this->operator->hold($order); + } + + public function testItSells(): void + { + $bundleVariant1 = $this->createMock(ProductVariantInterface::class); + $bundleVariant1 + ->method('isTracked') + ->willReturn(true); + $bundleVariant1 + ->method('getOnHold') + ->willReturn(10); + $bundleVariant1 + ->method('getOnHand') + ->willReturn(20); + $bundleVariant1 + ->method('setOnHold') + ->with(6); + $bundleVariant1 + ->method('setOnHand') + ->with(16); + + $bundleOrderItem = $this->createMock(OrderItemInterface::class); + $bundleOrderItem + ->expects(self::once()) + ->method('getProduct') + ->willReturn($this->bundle); + + $bundleOrderItem + ->expects(self::once()) + ->method('getQuantity') + ->willReturn(2); + + $bundleOrderItem + ->expects(self::once()) + ->method('getProductBundleOrderItems') + ->willReturn([ + $this->mockProductBundleOrderItem($bundleVariant1, 2), + ]); + $bundleOrderItem->expects(self::never())->method('getVariant'); + + $regularOrderItem = $this->createMock(OrderItemInterface::class); + $regularOrderItem + ->expects(self::once()) + ->method('getProduct') + ->willReturn($this->regularProduct); + $regularOrderItem + ->expects(self::once()) + ->method('getQuantity') + ->willReturn(3); + + $variant = $this->createMock(ProductVariantInterface::class); + $variant + ->method('isTracked') + ->willReturn(true); + $variant + ->method('getOnHold') + ->willReturn(100); + $variant + ->method('setOnHold') + ->with(97); + $variant + ->method('getOnHand') + ->willReturn(200); + $variant + ->method('setOnHand') + ->with(197); + + $regularOrderItem + ->expects(self::once()) + ->method('getVariant') + ->willReturn($variant); + + $regularOrderItem->expects(self::never())->method('getProductBundleOrderItems'); + + $order = $this->mockOrder( + $regularOrderItem, + $bundleOrderItem, + ); + + $this->operator->sell($order); + } + + public function testItReleases(): void + { + $bundleVariant1 = $this->createMock(ProductVariantInterface::class); + $bundleVariant1 + ->method('isTracked') + ->willReturn(true); + $bundleVariant1 + ->method('getOnHold') + ->willReturn(10); + $bundleVariant1 + ->method('setOnHold') + ->with(6); + + $bundleOrderItem = $this->createMock(OrderItemInterface::class); + $bundleOrderItem + ->expects(self::once()) + ->method('getProduct') + ->willReturn($this->bundle); + + $bundleOrderItem + ->expects(self::once()) + ->method('getQuantity') + ->willReturn(2); + + $bundleOrderItem + ->expects(self::once()) + ->method('getProductBundleOrderItems') + ->willReturn([ + $this->mockProductBundleOrderItem($bundleVariant1, 2), + ]); + $bundleOrderItem->expects(self::never())->method('getVariant'); + + $regularOrderItem = $this->createMock(OrderItemInterface::class); + $regularOrderItem + ->expects(self::once()) + ->method('getProduct') + ->willReturn($this->regularProduct); + $regularOrderItem + ->expects(self::once()) + ->method('getQuantity') + ->willReturn(3); + + $variant = $this->createMock(ProductVariantInterface::class); + $variant + ->method('isTracked') + ->willReturn(true); + $variant + ->method('getOnHold') + ->willReturn(100); + $variant + ->method('setOnHold') + ->with(97); + + $regularOrderItem + ->expects(self::once()) + ->method('getVariant') + ->willReturn($variant); + + $regularOrderItem->expects(self::never())->method('getProductBundleOrderItems'); + + $order = $this->mockOrder( + $regularOrderItem, + $bundleOrderItem, + ); + + $this->operator->release($order); + } + + public function testItGivesBack(): void + { + $bundleVariant1 = $this->createMock(ProductVariantInterface::class); + $bundleVariant1 + ->method('isTracked') + ->willReturn(true); + $bundleVariant1 + ->method('getOnHand') + ->willReturn(10); + $bundleVariant1 + ->method('setOnHand') + ->with(14); + + $bundleOrderItem = $this->createMock(OrderItemInterface::class); + $bundleOrderItem + ->expects(self::once()) + ->method('getProduct') + ->willReturn($this->bundle); + + $bundleOrderItem + ->expects(self::once()) + ->method('getQuantity') + ->willReturn(2); + + $bundleOrderItem + ->expects(self::once()) + ->method('getProductBundleOrderItems') + ->willReturn([ + $this->mockProductBundleOrderItem($bundleVariant1, 2), + ]); + $bundleOrderItem->expects(self::never())->method('getVariant'); + + $regularOrderItem = $this->createMock(OrderItemInterface::class); + $regularOrderItem + ->expects(self::once()) + ->method('getProduct') + ->willReturn($this->regularProduct); + $regularOrderItem + ->expects(self::once()) + ->method('getQuantity') + ->willReturn(3); + + $variant = $this->createMock(ProductVariantInterface::class); + $variant + ->method('isTracked') + ->willReturn(true); + $variant + ->method('getOnHand') + ->willReturn(100); + $variant + ->method('setOnHand') + ->with(103); + + $regularOrderItem + ->expects(self::once()) + ->method('getVariant') + ->willReturn($variant); + + $regularOrderItem->expects(self::never())->method('getProductBundleOrderItems'); + + $order = $this->mockOrder( + $regularOrderItem, + $bundleOrderItem, + ); + + $this->operator->giveBack($order); + } + + private function mockProductBundleOrderItem( + ProductVariantInterface $variant, + int $quantity, + ): ProductBundleOrderItemInterface { + $bundleOrderItem = $this->createMock(ProductBundleOrderItemInterface::class); + $bundleOrderItem + ->method('getProductVariant') + ->willReturn($variant); + $bundleOrderItem + ->method('getQuantity') + ->willReturn($quantity); + + return $bundleOrderItem; + } + + private function mockOrder(OrderItemInterface ...$items): OrderInterface + { + $order = $this->createMock(OrderInterface::class); + $order + ->method('getItems') + ->willReturn(new ArrayCollection($items)); + + return $order; + } +} diff --git a/tests/Unit/MotherObject/AddProductBundleItemToCartCommandMother.php b/tests/Unit/MotherObject/AddProductBundleItemToCartCommandMother.php index 6912c547..0d296c5d 100644 --- a/tests/Unit/MotherObject/AddProductBundleItemToCartCommandMother.php +++ b/tests/Unit/MotherObject/AddProductBundleItemToCartCommandMother.php @@ -12,11 +12,12 @@ namespace Tests\BitBag\SyliusProductBundlePlugin\Unit\MotherObject; use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommand; +use BitBag\SyliusProductBundlePlugin\Command\AddProductBundleItemToCartCommandInterface; use BitBag\SyliusProductBundlePlugin\Entity\ProductBundleItemInterface; final class AddProductBundleItemToCartCommandMother { - public static function create(ProductBundleItemInterface $bundleItem): AddProductBundleItemToCartCommand + public static function create(ProductBundleItemInterface $bundleItem): AddProductBundleItemToCartCommandInterface { return new AddProductBundleItemToCartCommand($bundleItem); } diff --git a/tests/Unit/Provider/AddProductBundleItemToCartCommandProviderTest.php b/tests/Unit/Provider/AddProductBundleItemToCartCommandProviderTest.php new file mode 100644 index 00000000..de5b8492 --- /dev/null +++ b/tests/Unit/Provider/AddProductBundleItemToCartCommandProviderTest.php @@ -0,0 +1,189 @@ +addProductBundleItemToCartCommandFactory = $this->createMock(AddProductBundleItemToCartCommandFactoryInterface::class); + $this->productBundleRepository = $this->createMock(ProductBundleRepositoryInterface::class); + $this->productVariantRepository = $this->createMock(ProductVariantRepositoryInterface::class); + + $this->bundleItem1 = $this->createMock(ProductBundleItemInterface::class); + $this->bundleItem2 = $this->createMock(ProductBundleItemInterface::class); + $this->bundle = $this->createMock(ProductBundleInterface::class); + $this->bundle + ->expects(self::any()) + ->method('getProductBundleItems') + ->willReturn(new ArrayCollection([$this->bundleItem1, $this->bundleItem2])); + + $this->provider = new AddProductBundleItemToCartCommandProvider( + $this->addProductBundleItemToCartCommandFactory, + $this->productBundleRepository, + $this->productVariantRepository, + ); + } + + public function testItThrowsExceptionIfBundleIsNotFound(): void + { + self::expectException(\Exception::class); + self::expectExceptionMessage('Product bundle not found'); + + $this->productBundleRepository + ->expects(self::once()) + ->method('findOneByProductCode') + ->with('BUNDLE_CODE') + ->willReturn(null); + + $this->provider->provide('BUNDLE_CODE', []); + } + + public function testItWillNotOverwriteIfBundleIsPacked(): void + { + $this->bundle + ->expects(self::exactly(2)) + ->method('isPackedProduct') + ->willReturn(true); + + $this->productBundleRepository + ->expects(self::once()) + ->method('findOneByProductCode') + ->with('BUNDLE_CODE') + ->willReturn($this->bundle); + + $addProductBundleItemToCartCommand = $this->createMock(AddProductBundleItemToCartCommandInterface::class); + + $this->addProductBundleItemToCartCommandFactory + ->expects(self::exactly(2)) + ->method('createNew') + ->withConsecutive([$this->bundleItem1], [$this->bundleItem2]) + ->willReturn($addProductBundleItemToCartCommand); + + $this->productVariantRepository->expects(self::never())->method(self::anything()); + + $this->provider->provide('BUNDLE_CODE', []); + } + + public function testItWillNotOverwriteIfOverwrittenVariantsIsEmpty(): void + { + $this->bundle + ->expects(self::exactly(2)) + ->method('isPackedProduct') + ->willReturn(false); + + $this->productBundleRepository + ->expects(self::once()) + ->method('findOneByProductCode') + ->with('BUNDLE_CODE') + ->willReturn($this->bundle); + + $addProductBundleItemToCartCommand = $this->createMock(AddProductBundleItemToCartCommandInterface::class); + + $this->addProductBundleItemToCartCommandFactory + ->expects(self::exactly(2)) + ->method('createNew') + ->withConsecutive([$this->bundleItem1], [$this->bundleItem2]) + ->willReturn($addProductBundleItemToCartCommand); + + $this->productVariantRepository->expects(self::never())->method(self::anything()); + + $this->provider->provide('BUNDLE_CODE', []); + } + + public function testItOverwrites(): void + { + $this->bundle + ->expects(self::exactly(2)) + ->method('isPackedProduct') + ->willReturn(false); + + $this->productBundleRepository + ->expects(self::once()) + ->method('findOneByProductCode') + ->with('BUNDLE_CODE') + ->willReturn($this->bundle); + + $product = $this->createMock(ProductInterface::class); + + $oldProductVariant = $this->createMock(ProductVariantInterface::class); + $oldProductVariant + ->expects(self::once()) + ->method('getCode') + ->willReturn('OLD_VARIANT_CODE'); + $oldProductVariant + ->expects(self::once()) + ->method('getProduct') + ->willReturn($product); + + $newProductVariant = $this->createMock(ProductVariantInterface::class); + $newProductVariant + ->expects(self::once()) + ->method('getProduct') + ->willReturn($product); + + $this->bundleItem1 + ->expects(self::once()) + ->method('getProductVariant') + ->willReturn($oldProductVariant); + + $this->productVariantRepository + ->expects(self::exactly(3)) + ->method('findOneBy') + ->willReturnOnConsecutiveCalls($oldProductVariant, $newProductVariant, $newProductVariant); + + $addProductBundleItemToCartCommand = $this->createMock(AddProductBundleItemToCartCommandInterface::class); + + $this->addProductBundleItemToCartCommandFactory + ->expects(self::exactly(2)) + ->method('createNew') + ->withConsecutive([$this->bundleItem1], [$this->bundleItem2]) + ->willReturn($addProductBundleItemToCartCommand); + + $overwrittenVariants = [ + [ + 'from' => 'OLD_VARIANT_CODE', + 'to' => 'NEW_VARIANT_CODE', + ], + ]; + + $this->provider->provide('BUNDLE_CODE', $overwrittenVariants); + } +}