Repository files navigation

build:started

This plugin adds the option of configuring a minimum order value for each channel.

When a minimum order value is configured it is not possible to checkout the order if the subtotal of all of the items in the cart is lower the configured minimum.

Configuration by administrator:

Configuring a minimum order value is possible by logging in as an administrator and navigating to the channel configuration:

admin_menu

Choose to edit an existing channel or to add a new one:

admin_channel_overview

This plugin adds a segment to the add/edit pane that can be used to set the desired minimum order value in the base currency:

admin_channel_details

By default the toggle is set to the off position and the field to enter the minimum order value is disabled. To configure the desired value use the toggle to enable the minimum order value and enter the value in the field.

What do visitors see?

When visiting the shop and not having added enough items to the shopping cart they will not be able to proceed to checkout:

shop_cart_summary

The checkout button is disabled and a message is displayed, providing more insight in the current status:

shop_minimum_order_value_message

When clicking the cart widget button, the checkout button is hidden:

shop_cart_widget

If a visitor tries to skip ahead to the checkout process (for example by entering the url manually), the visitor will automatically be redirected to the cart summary page.

How does it work?

Technically what this plugin does is a few things. It adds an optional field to the channel entity and the option to configure the fields value through the admin system.

This value is used by the MinimumOrderValueReached class, which is configured as a guard on all of the transitions of the "sylius_order_checkout" state machine.

By overriding the "sylius.resolver.checkout" service and disabling the original service, the plugin is able to redirect the visitor to the cart summary page when the guard blocks the transition.

Overriding that service also means that the service needs to be configured slightly differently then before (only applicable if you have a customised checkout resolver configuration):

Whereas before the the checkout resolver could be configured through key "sylius_shop", it now needs to be done through key "nedac_sylius_minimum_order_value":

nedac_sylius_minimum_order_value:
checkout_resolver:
pattern: /checkout/.+route_map:
empty_order:
route: sylius_shop_cart_summarycart:
route: sylius_shop_checkout_addressaddressed:
route: sylius_shop_checkout_select_shippingshipping_selected:
route: sylius_shop_checkout_select_paymentshipping_skipped:
route: sylius_shop_checkout_select_paymentpayment_selected:
route: sylius_shop_checkout_completepayment_skipped:
route: sylius_shop_checkout_complete

This is the default configuration added by this plugin in src/Resources/config/config.yaml. It can be overridden on an application level as with any Symfony bundle configuration.

Please see the official Sylius docs on how to configure the checkout resolver, should there be any need to do so.

Supported Sylius versions:
1.6
1.7

NOTE:This plugin requires PHP 7.4 or up

Installation:

  1. Install using composer:

    composer require nedac/sylius-minimum-order-value-plugin
  2. Add to bundles.php:

    # config/bundles.php<?phpreturn [
    # ...Nedac\SyliusMinimumOrderValuePlugin\NedacSyliusMinimumOrderValuePlugin::class => ['all' => true],
    ];
  3. Import configuration file:

    # config/packages/_sylius.yamlimports:
    # ...
    - { resource: "@NedacSyliusMinimumOrderValuePlugin/Resources/config/config.yaml" }# ...
  4. Implement the interface and use the trait in the Channel model:

    # src/entity/Channel/Channel.php<?phpdeclare(strict_types=1);
    namespaceApp\Entity\Channel;
    useDoctrine\ORM\MappingasORM;
    useNedac\SyliusMinimumOrderValuePlugin\Model\ChannelInterfaceasNedacSyliusMinimumOrderValuePluginChannelInterface;
    useNedac\SyliusMinimumOrderValuePlugin\Model\MinimumOrderValueTraitasNedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    useSylius\Component\Core\Model\ChannelasBaseChannel;
    /** * @ORM\Entity * @ORM\Table(name="sylius_channel") */class Channel extends BaseChannel implements NedacSyliusMinimumOrderValuePluginChannelInterface
    {
    use NedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    // ...
    }

    If the file src/entity/Channel/Channel.php does not yet exit, please create it using this example. When you do, also add the following section to config/packages/_sylius.yaml:

    sylius_channel:
    resources:
    channel:
    classes:
    model: App\Entity\Channel\Channel
  5. Generate and run database migration:

    bin/console doctrine:migrations:diff
    bin/console doctrine:migrations:migrate
  6. Override templates (For Sylius 1.7 skip this step):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <divclass="ui segment">
    {{ form_errors(form) }}
    <divclass="two fields">
    {{ form_row(form.code) }}
    {{ form_row(form.name) }}
    </div>
    {{ form_row(form.description) }}
    {{ form_row(form.enabled) }}
    <divclass="two fields">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.color) }}
    {{ form_row(form.themeName) }}
    </div>
    </div>
    <divclass="ui segment">
    <h4class="ui dividing header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    </div>
    <divclass="column">
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    {{ form_row(form.currencies) }}
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/_widget.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_widget_content', {'cart': cart}) }}
    <iclass="cart icon"></i>
    <spanid="sylius-cart-total">
    {{ money.convertAndFormat(cart.itemsTotal) }}
    </span>
    {% transchoicecart.items|length %}sylius.ui.item.choice{% endtranschoice %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_widget_content', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_popup_content', {'cart': cart}) }}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_popup_content', {'cart': cart}) }}
    </div>
  7. Override templates (Sylius 1.7 only, skip this step for Sylius 1.6):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <h4class="ui top attached large header">{{ 'sylius.ui.general'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_errors(form) }}
    <divclass="fields">
    <divclass="six wide field">
    {{ form_row(form.code) }}
    </div>
    <divclass="seven wide field">
    {{ form_row(form.name) }}
    </div>
    <divclass="three wide field">
    {{ form_row(form.color) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    {{ form_row(form.enabled) }}
    </div>
    <divclass="ui attached segment">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    {{ form_row(form.description, {'attr': {'rows' : '3'}}) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.countries) }}
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.money'|trans }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.currencies) }}
    </div>
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    </div>
    </div>
    <divclass="column">
    <h4class="ui top attached large header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.look_and_feel'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_row(form.themeName) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.menuTaxon) }}
    </div>
    <divclass="ui hidden divider"></div>
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui attached segment">
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/Widget/_popup.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sylius_template_event('sylius.shop.cart.widget.button', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sylius_template_event('sylius.shop.cart.widget.popup', {'cart': cart}) }}
    </div>
  8. Install assets:

    bin/console sylius:install:assets

Setup development environment:

docker-compose build
docker-compose up -d
docker-compose exec php composer --working-dir=/srv/sylius install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application build
docker-compose exec php bin/console assets:install public
docker-compose exec php bin/console doctrine:schema:create
docker-compose exec php bin/console sylius:fixtures:load -n

Running tests:

docker-compose exec php sh
bin/console doc:sche:cre
cd ../..
vendor/bin/phpcs
vendor/bin/phpstan analyse src/ --level max
vendor/bin/phpunit
vendor/bin/behat

About

With this Sylius plugin a minimum order value can be set for each channel.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

build:started

This plugin adds the option of configuring a minimum order value for each channel.

When a minimum order value is configured it is not possible to checkout the order if the subtotal of all of the items in the cart is lower the configured minimum.

Configuration by administrator:

Configuring a minimum order value is possible by logging in as an administrator and navigating to the channel configuration:

admin_menu

Choose to edit an existing channel or to add a new one:

admin_channel_overview

This plugin adds a segment to the add/edit pane that can be used to set the desired minimum order value in the base currency:

admin_channel_details

By default the toggle is set to the off position and the field to enter the minimum order value is disabled. To configure the desired value use the toggle to enable the minimum order value and enter the value in the field.

What do visitors see?

When visiting the shop and not having added enough items to the shopping cart they will not be able to proceed to checkout:

shop_cart_summary

The checkout button is disabled and a message is displayed, providing more insight in the current status:

shop_minimum_order_value_message

When clicking the cart widget button, the checkout button is hidden:

shop_cart_widget

If a visitor tries to skip ahead to the checkout process (for example by entering the url manually), the visitor will automatically be redirected to the cart summary page.

How does it work?

Technically what this plugin does is a few things. It adds an optional field to the channel entity and the option to configure the fields value through the admin system.

This value is used by the MinimumOrderValueReached class, which is configured as a guard on all of the transitions of the "sylius_order_checkout" state machine.

By overriding the "sylius.resolver.checkout" service and disabling the original service, the plugin is able to redirect the visitor to the cart summary page when the guard blocks the transition.

Overriding that service also means that the service needs to be configured slightly differently then before (only applicable if you have a customised checkout resolver configuration):

Whereas before the the checkout resolver could be configured through key "sylius_shop", it now needs to be done through key "nedac_sylius_minimum_order_value":

nedac_sylius_minimum_order_value:
checkout_resolver:
pattern: /checkout/.+route_map:
empty_order:
route: sylius_shop_cart_summarycart:
route: sylius_shop_checkout_addressaddressed:
route: sylius_shop_checkout_select_shippingshipping_selected:
route: sylius_shop_checkout_select_paymentshipping_skipped:
route: sylius_shop_checkout_select_paymentpayment_selected:
route: sylius_shop_checkout_completepayment_skipped:
route: sylius_shop_checkout_complete

This is the default configuration added by this plugin in src/Resources/config/config.yaml. It can be overridden on an application level as with any Symfony bundle configuration.

Please see the official Sylius docs on how to configure the checkout resolver, should there be any need to do so.

Supported Sylius versions:
1.6
1.7

NOTE:This plugin requires PHP 7.4 or up

Installation:

  1. Install using composer:

    composer require nedac/sylius-minimum-order-value-plugin
  2. Add to bundles.php:

    # config/bundles.php<?phpreturn [
    # ...Nedac\SyliusMinimumOrderValuePlugin\NedacSyliusMinimumOrderValuePlugin::class => ['all' => true],
    ];
  3. Import configuration file:

    # config/packages/_sylius.yamlimports:
    # ...
    - { resource: "@NedacSyliusMinimumOrderValuePlugin/Resources/config/config.yaml" }# ...
  4. Implement the interface and use the trait in the Channel model:

    # src/entity/Channel/Channel.php<?phpdeclare(strict_types=1);
    namespaceApp\Entity\Channel;
    useDoctrine\ORM\MappingasORM;
    useNedac\SyliusMinimumOrderValuePlugin\Model\ChannelInterfaceasNedacSyliusMinimumOrderValuePluginChannelInterface;
    useNedac\SyliusMinimumOrderValuePlugin\Model\MinimumOrderValueTraitasNedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    useSylius\Component\Core\Model\ChannelasBaseChannel;
    /** * @ORM\Entity * @ORM\Table(name="sylius_channel") */class Channel extends BaseChannel implements NedacSyliusMinimumOrderValuePluginChannelInterface
    {
    use NedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    // ...
    }

    If the file src/entity/Channel/Channel.php does not yet exit, please create it using this example. When you do, also add the following section to config/packages/_sylius.yaml:

    sylius_channel:
    resources:
    channel:
    classes:
    model: App\Entity\Channel\Channel
  5. Generate and run database migration:

    bin/console doctrine:migrations:diff
    bin/console doctrine:migrations:migrate
  6. Override templates (For Sylius 1.7 skip this step):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <divclass="ui segment">
    {{ form_errors(form) }}
    <divclass="two fields">
    {{ form_row(form.code) }}
    {{ form_row(form.name) }}
    </div>
    {{ form_row(form.description) }}
    {{ form_row(form.enabled) }}
    <divclass="two fields">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.color) }}
    {{ form_row(form.themeName) }}
    </div>
    </div>
    <divclass="ui segment">
    <h4class="ui dividing header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    </div>
    <divclass="column">
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    {{ form_row(form.currencies) }}
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/_widget.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_widget_content', {'cart': cart}) }}
    <iclass="cart icon"></i>
    <spanid="sylius-cart-total">
    {{ money.convertAndFormat(cart.itemsTotal) }}
    </span>
    {% transchoicecart.items|length %}sylius.ui.item.choice{% endtranschoice %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_widget_content', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_popup_content', {'cart': cart}) }}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_popup_content', {'cart': cart}) }}
    </div>
  7. Override templates (Sylius 1.7 only, skip this step for Sylius 1.6):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <h4class="ui top attached large header">{{ 'sylius.ui.general'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_errors(form) }}
    <divclass="fields">
    <divclass="six wide field">
    {{ form_row(form.code) }}
    </div>
    <divclass="seven wide field">
    {{ form_row(form.name) }}
    </div>
    <divclass="three wide field">
    {{ form_row(form.color) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    {{ form_row(form.enabled) }}
    </div>
    <divclass="ui attached segment">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    {{ form_row(form.description, {'attr': {'rows' : '3'}}) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.countries) }}
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.money'|trans }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.currencies) }}
    </div>
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    </div>
    </div>
    <divclass="column">
    <h4class="ui top attached large header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.look_and_feel'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_row(form.themeName) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.menuTaxon) }}
    </div>
    <divclass="ui hidden divider"></div>
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui attached segment">
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/Widget/_popup.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sylius_template_event('sylius.shop.cart.widget.button', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sylius_template_event('sylius.shop.cart.widget.popup', {'cart': cart}) }}
    </div>
  8. Install assets:

    bin/console sylius:install:assets

Setup development environment:

docker-compose build
docker-compose up -d
docker-compose exec php composer --working-dir=/srv/sylius install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application build
docker-compose exec php bin/console assets:install public
docker-compose exec php bin/console doctrine:schema:create
docker-compose exec php bin/console sylius:fixtures:load -n

Running tests:

docker-compose exec php sh
bin/console doc:sche:cre
cd ../..
vendor/bin/phpcs
vendor/bin/phpstan analyse src/ --level max
vendor/bin/phpunit
vendor/bin/behat

About

With this Sylius plugin a minimum order value can be set for each channel.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

build:started

This plugin adds the option of configuring a minimum order value for each channel.

When a minimum order value is configured it is not possible to checkout the order if the subtotal of all of the items in the cart is lower the configured minimum.

Configuration by administrator:

Configuring a minimum order value is possible by logging in as an administrator and navigating to the channel configuration:

admin_menu

Choose to edit an existing channel or to add a new one:

admin_channel_overview

This plugin adds a segment to the add/edit pane that can be used to set the desired minimum order value in the base currency:

admin_channel_details

By default the toggle is set to the off position and the field to enter the minimum order value is disabled. To configure the desired value use the toggle to enable the minimum order value and enter the value in the field.

What do visitors see?

When visiting the shop and not having added enough items to the shopping cart they will not be able to proceed to checkout:

shop_cart_summary

The checkout button is disabled and a message is displayed, providing more insight in the current status:

shop_minimum_order_value_message

When clicking the cart widget button, the checkout button is hidden:

shop_cart_widget

If a visitor tries to skip ahead to the checkout process (for example by entering the url manually), the visitor will automatically be redirected to the cart summary page.

How does it work?

Technically what this plugin does is a few things. It adds an optional field to the channel entity and the option to configure the fields value through the admin system.

This value is used by the MinimumOrderValueReached class, which is configured as a guard on all of the transitions of the "sylius_order_checkout" state machine.

By overriding the "sylius.resolver.checkout" service and disabling the original service, the plugin is able to redirect the visitor to the cart summary page when the guard blocks the transition.

Overriding that service also means that the service needs to be configured slightly differently then before (only applicable if you have a customised checkout resolver configuration):

Whereas before the the checkout resolver could be configured through key "sylius_shop", it now needs to be done through key "nedac_sylius_minimum_order_value":

nedac_sylius_minimum_order_value:
checkout_resolver:
pattern: /checkout/.+route_map:
empty_order:
route: sylius_shop_cart_summarycart:
route: sylius_shop_checkout_addressaddressed:
route: sylius_shop_checkout_select_shippingshipping_selected:
route: sylius_shop_checkout_select_paymentshipping_skipped:
route: sylius_shop_checkout_select_paymentpayment_selected:
route: sylius_shop_checkout_completepayment_skipped:
route: sylius_shop_checkout_complete

This is the default configuration added by this plugin in src/Resources/config/config.yaml. It can be overridden on an application level as with any Symfony bundle configuration.

Please see the official Sylius docs on how to configure the checkout resolver, should there be any need to do so.

Supported Sylius versions:
1.6
1.7

NOTE:This plugin requires PHP 7.4 or up

Installation:

  1. Install using composer:

    composer require nedac/sylius-minimum-order-value-plugin
  2. Add to bundles.php:

    # config/bundles.php<?phpreturn [
    # ...Nedac\SyliusMinimumOrderValuePlugin\NedacSyliusMinimumOrderValuePlugin::class => ['all' => true],
    ];
  3. Import configuration file:

    # config/packages/_sylius.yamlimports:
    # ...
    - { resource: "@NedacSyliusMinimumOrderValuePlugin/Resources/config/config.yaml" }# ...
  4. Implement the interface and use the trait in the Channel model:

    # src/entity/Channel/Channel.php<?phpdeclare(strict_types=1);
    namespaceApp\Entity\Channel;
    useDoctrine\ORM\MappingasORM;
    useNedac\SyliusMinimumOrderValuePlugin\Model\ChannelInterfaceasNedacSyliusMinimumOrderValuePluginChannelInterface;
    useNedac\SyliusMinimumOrderValuePlugin\Model\MinimumOrderValueTraitasNedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    useSylius\Component\Core\Model\ChannelasBaseChannel;
    /** * @ORM\Entity * @ORM\Table(name="sylius_channel") */class Channel extends BaseChannel implements NedacSyliusMinimumOrderValuePluginChannelInterface
    {
    use NedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    // ...
    }

    If the file src/entity/Channel/Channel.php does not yet exit, please create it using this example. When you do, also add the following section to config/packages/_sylius.yaml:

    sylius_channel:
    resources:
    channel:
    classes:
    model: App\Entity\Channel\Channel
  5. Generate and run database migration:

    bin/console doctrine:migrations:diff
    bin/console doctrine:migrations:migrate
  6. Override templates (For Sylius 1.7 skip this step):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <divclass="ui segment">
    {{ form_errors(form) }}
    <divclass="two fields">
    {{ form_row(form.code) }}
    {{ form_row(form.name) }}
    </div>
    {{ form_row(form.description) }}
    {{ form_row(form.enabled) }}
    <divclass="two fields">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.color) }}
    {{ form_row(form.themeName) }}
    </div>
    </div>
    <divclass="ui segment">
    <h4class="ui dividing header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    </div>
    <divclass="column">
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    {{ form_row(form.currencies) }}
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/_widget.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_widget_content', {'cart': cart}) }}
    <iclass="cart icon"></i>
    <spanid="sylius-cart-total">
    {{ money.convertAndFormat(cart.itemsTotal) }}
    </span>
    {% transchoicecart.items|length %}sylius.ui.item.choice{% endtranschoice %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_widget_content', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_popup_content', {'cart': cart}) }}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_popup_content', {'cart': cart}) }}
    </div>
  7. Override templates (Sylius 1.7 only, skip this step for Sylius 1.6):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <h4class="ui top attached large header">{{ 'sylius.ui.general'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_errors(form) }}
    <divclass="fields">
    <divclass="six wide field">
    {{ form_row(form.code) }}
    </div>
    <divclass="seven wide field">
    {{ form_row(form.name) }}
    </div>
    <divclass="three wide field">
    {{ form_row(form.color) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    {{ form_row(form.enabled) }}
    </div>
    <divclass="ui attached segment">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    {{ form_row(form.description, {'attr': {'rows' : '3'}}) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.countries) }}
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.money'|trans }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.currencies) }}
    </div>
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    </div>
    </div>
    <divclass="column">
    <h4class="ui top attached large header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.look_and_feel'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_row(form.themeName) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.menuTaxon) }}
    </div>
    <divclass="ui hidden divider"></div>
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui attached segment">
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/Widget/_popup.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sylius_template_event('sylius.shop.cart.widget.button', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sylius_template_event('sylius.shop.cart.widget.popup', {'cart': cart}) }}
    </div>
  8. Install assets:

    bin/console sylius:install:assets

Setup development environment:

docker-compose build
docker-compose up -d
docker-compose exec php composer --working-dir=/srv/sylius install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application build
docker-compose exec php bin/console assets:install public
docker-compose exec php bin/console doctrine:schema:create
docker-compose exec php bin/console sylius:fixtures:load -n

Running tests:

docker-compose exec php sh
bin/console doc:sche:cre
cd ../..
vendor/bin/phpcs
vendor/bin/phpstan analyse src/ --level max
vendor/bin/phpunit
vendor/bin/behat

About

With this Sylius plugin a minimum order value can be set for each channel.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

build:started

This plugin adds the option of configuring a minimum order value for each channel.

When a minimum order value is configured it is not possible to checkout the order if the subtotal of all of the items in the cart is lower the configured minimum.

Configuration by administrator:

Configuring a minimum order value is possible by logging in as an administrator and navigating to the channel configuration:

admin_menu

Choose to edit an existing channel or to add a new one:

admin_channel_overview

This plugin adds a segment to the add/edit pane that can be used to set the desired minimum order value in the base currency:

admin_channel_details

By default the toggle is set to the off position and the field to enter the minimum order value is disabled. To configure the desired value use the toggle to enable the minimum order value and enter the value in the field.

What do visitors see?

When visiting the shop and not having added enough items to the shopping cart they will not be able to proceed to checkout:

shop_cart_summary

The checkout button is disabled and a message is displayed, providing more insight in the current status:

shop_minimum_order_value_message

When clicking the cart widget button, the checkout button is hidden:

shop_cart_widget

If a visitor tries to skip ahead to the checkout process (for example by entering the url manually), the visitor will automatically be redirected to the cart summary page.

How does it work?

Technically what this plugin does is a few things. It adds an optional field to the channel entity and the option to configure the fields value through the admin system.

This value is used by the MinimumOrderValueReached class, which is configured as a guard on all of the transitions of the "sylius_order_checkout" state machine.

By overriding the "sylius.resolver.checkout" service and disabling the original service, the plugin is able to redirect the visitor to the cart summary page when the guard blocks the transition.

Overriding that service also means that the service needs to be configured slightly differently then before (only applicable if you have a customised checkout resolver configuration):

Whereas before the the checkout resolver could be configured through key "sylius_shop", it now needs to be done through key "nedac_sylius_minimum_order_value":

nedac_sylius_minimum_order_value:
checkout_resolver:
pattern: /checkout/.+route_map:
empty_order:
route: sylius_shop_cart_summarycart:
route: sylius_shop_checkout_addressaddressed:
route: sylius_shop_checkout_select_shippingshipping_selected:
route: sylius_shop_checkout_select_paymentshipping_skipped:
route: sylius_shop_checkout_select_paymentpayment_selected:
route: sylius_shop_checkout_completepayment_skipped:
route: sylius_shop_checkout_complete

This is the default configuration added by this plugin in src/Resources/config/config.yaml. It can be overridden on an application level as with any Symfony bundle configuration.

Please see the official Sylius docs on how to configure the checkout resolver, should there be any need to do so.

Supported Sylius versions:
1.6
1.7

NOTE:This plugin requires PHP 7.4 or up

Installation:

  1. Install using composer:

    composer require nedac/sylius-minimum-order-value-plugin
  2. Add to bundles.php:

    # config/bundles.php<?phpreturn [
    # ...Nedac\SyliusMinimumOrderValuePlugin\NedacSyliusMinimumOrderValuePlugin::class => ['all' => true],
    ];
  3. Import configuration file:

    # config/packages/_sylius.yamlimports:
    # ...
    - { resource: "@NedacSyliusMinimumOrderValuePlugin/Resources/config/config.yaml" }# ...
  4. Implement the interface and use the trait in the Channel model:

    # src/entity/Channel/Channel.php<?phpdeclare(strict_types=1);
    namespaceApp\Entity\Channel;
    useDoctrine\ORM\MappingasORM;
    useNedac\SyliusMinimumOrderValuePlugin\Model\ChannelInterfaceasNedacSyliusMinimumOrderValuePluginChannelInterface;
    useNedac\SyliusMinimumOrderValuePlugin\Model\MinimumOrderValueTraitasNedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    useSylius\Component\Core\Model\ChannelasBaseChannel;
    /** * @ORM\Entity * @ORM\Table(name="sylius_channel") */class Channel extends BaseChannel implements NedacSyliusMinimumOrderValuePluginChannelInterface
    {
    use NedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    // ...
    }

    If the file src/entity/Channel/Channel.php does not yet exit, please create it using this example. When you do, also add the following section to config/packages/_sylius.yaml:

    sylius_channel:
    resources:
    channel:
    classes:
    model: App\Entity\Channel\Channel
  5. Generate and run database migration:

    bin/console doctrine:migrations:diff
    bin/console doctrine:migrations:migrate
  6. Override templates (For Sylius 1.7 skip this step):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <divclass="ui segment">
    {{ form_errors(form) }}
    <divclass="two fields">
    {{ form_row(form.code) }}
    {{ form_row(form.name) }}
    </div>
    {{ form_row(form.description) }}
    {{ form_row(form.enabled) }}
    <divclass="two fields">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.color) }}
    {{ form_row(form.themeName) }}
    </div>
    </div>
    <divclass="ui segment">
    <h4class="ui dividing header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    </div>
    <divclass="column">
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    {{ form_row(form.currencies) }}
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/_widget.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_widget_content', {'cart': cart}) }}
    <iclass="cart icon"></i>
    <spanid="sylius-cart-total">
    {{ money.convertAndFormat(cart.itemsTotal) }}
    </span>
    {% transchoicecart.items|length %}sylius.ui.item.choice{% endtranschoice %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_widget_content', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_popup_content', {'cart': cart}) }}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_popup_content', {'cart': cart}) }}
    </div>
  7. Override templates (Sylius 1.7 only, skip this step for Sylius 1.6):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <h4class="ui top attached large header">{{ 'sylius.ui.general'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_errors(form) }}
    <divclass="fields">
    <divclass="six wide field">
    {{ form_row(form.code) }}
    </div>
    <divclass="seven wide field">
    {{ form_row(form.name) }}
    </div>
    <divclass="three wide field">
    {{ form_row(form.color) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    {{ form_row(form.enabled) }}
    </div>
    <divclass="ui attached segment">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    {{ form_row(form.description, {'attr': {'rows' : '3'}}) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.countries) }}
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.money'|trans }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.currencies) }}
    </div>
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    </div>
    </div>
    <divclass="column">
    <h4class="ui top attached large header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.look_and_feel'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_row(form.themeName) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.menuTaxon) }}
    </div>
    <divclass="ui hidden divider"></div>
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui attached segment">
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/Widget/_popup.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sylius_template_event('sylius.shop.cart.widget.button', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sylius_template_event('sylius.shop.cart.widget.popup', {'cart': cart}) }}
    </div>
  8. Install assets:

    bin/console sylius:install:assets

Setup development environment:

docker-compose build
docker-compose up -d
docker-compose exec php composer --working-dir=/srv/sylius install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application build
docker-compose exec php bin/console assets:install public
docker-compose exec php bin/console doctrine:schema:create
docker-compose exec php bin/console sylius:fixtures:load -n

Running tests:

docker-compose exec php sh
bin/console doc:sche:cre
cd ../..
vendor/bin/phpcs
vendor/bin/phpstan analyse src/ --level max
vendor/bin/phpunit
vendor/bin/behat

About

With this Sylius plugin a minimum order value can be set for each channel.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

build:started

This plugin adds the option of configuring a minimum order value for each channel.

When a minimum order value is configured it is not possible to checkout the order if the subtotal of all of the items in the cart is lower the configured minimum.

Configuration by administrator:

Configuring a minimum order value is possible by logging in as an administrator and navigating to the channel configuration:

admin_menu

Choose to edit an existing channel or to add a new one:

admin_channel_overview

This plugin adds a segment to the add/edit pane that can be used to set the desired minimum order value in the base currency:

admin_channel_details

By default the toggle is set to the off position and the field to enter the minimum order value is disabled. To configure the desired value use the toggle to enable the minimum order value and enter the value in the field.

What do visitors see?

When visiting the shop and not having added enough items to the shopping cart they will not be able to proceed to checkout:

shop_cart_summary

The checkout button is disabled and a message is displayed, providing more insight in the current status:

shop_minimum_order_value_message

When clicking the cart widget button, the checkout button is hidden:

shop_cart_widget

If a visitor tries to skip ahead to the checkout process (for example by entering the url manually), the visitor will automatically be redirected to the cart summary page.

How does it work?

Technically what this plugin does is a few things. It adds an optional field to the channel entity and the option to configure the fields value through the admin system.

This value is used by the MinimumOrderValueReached class, which is configured as a guard on all of the transitions of the "sylius_order_checkout" state machine.

By overriding the "sylius.resolver.checkout" service and disabling the original service, the plugin is able to redirect the visitor to the cart summary page when the guard blocks the transition.

Overriding that service also means that the service needs to be configured slightly differently then before (only applicable if you have a customised checkout resolver configuration):

Whereas before the the checkout resolver could be configured through key "sylius_shop", it now needs to be done through key "nedac_sylius_minimum_order_value":

nedac_sylius_minimum_order_value:
checkout_resolver:
pattern: /checkout/.+route_map:
empty_order:
route: sylius_shop_cart_summarycart:
route: sylius_shop_checkout_addressaddressed:
route: sylius_shop_checkout_select_shippingshipping_selected:
route: sylius_shop_checkout_select_paymentshipping_skipped:
route: sylius_shop_checkout_select_paymentpayment_selected:
route: sylius_shop_checkout_completepayment_skipped:
route: sylius_shop_checkout_complete

This is the default configuration added by this plugin in src/Resources/config/config.yaml. It can be overridden on an application level as with any Symfony bundle configuration.

Please see the official Sylius docs on how to configure the checkout resolver, should there be any need to do so.

Supported Sylius versions:
1.6
1.7

NOTE:This plugin requires PHP 7.4 or up

Installation:

  1. Install using composer:

    composer require nedac/sylius-minimum-order-value-plugin
  2. Add to bundles.php:

    # config/bundles.php<?phpreturn [
    # ...Nedac\SyliusMinimumOrderValuePlugin\NedacSyliusMinimumOrderValuePlugin::class => ['all' => true],
    ];
  3. Import configuration file:

    # config/packages/_sylius.yamlimports:
    # ...
    - { resource: "@NedacSyliusMinimumOrderValuePlugin/Resources/config/config.yaml" }# ...
  4. Implement the interface and use the trait in the Channel model:

    # src/entity/Channel/Channel.php<?phpdeclare(strict_types=1);
    namespaceApp\Entity\Channel;
    useDoctrine\ORM\MappingasORM;
    useNedac\SyliusMinimumOrderValuePlugin\Model\ChannelInterfaceasNedacSyliusMinimumOrderValuePluginChannelInterface;
    useNedac\SyliusMinimumOrderValuePlugin\Model\MinimumOrderValueTraitasNedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    useSylius\Component\Core\Model\ChannelasBaseChannel;
    /** * @ORM\Entity * @ORM\Table(name="sylius_channel") */class Channel extends BaseChannel implements NedacSyliusMinimumOrderValuePluginChannelInterface
    {
    use NedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    // ...
    }

    If the file src/entity/Channel/Channel.php does not yet exit, please create it using this example. When you do, also add the following section to config/packages/_sylius.yaml:

    sylius_channel:
    resources:
    channel:
    classes:
    model: App\Entity\Channel\Channel
  5. Generate and run database migration:

    bin/console doctrine:migrations:diff
    bin/console doctrine:migrations:migrate
  6. Override templates (For Sylius 1.7 skip this step):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <divclass="ui segment">
    {{ form_errors(form) }}
    <divclass="two fields">
    {{ form_row(form.code) }}
    {{ form_row(form.name) }}
    </div>
    {{ form_row(form.description) }}
    {{ form_row(form.enabled) }}
    <divclass="two fields">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.color) }}
    {{ form_row(form.themeName) }}
    </div>
    </div>
    <divclass="ui segment">
    <h4class="ui dividing header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    </div>
    <divclass="column">
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    {{ form_row(form.currencies) }}
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/_widget.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_widget_content', {'cart': cart}) }}
    <iclass="cart icon"></i>
    <spanid="sylius-cart-total">
    {{ money.convertAndFormat(cart.itemsTotal) }}
    </span>
    {% transchoicecart.items|length %}sylius.ui.item.choice{% endtranschoice %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_widget_content', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_popup_content', {'cart': cart}) }}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_popup_content', {'cart': cart}) }}
    </div>
  7. Override templates (Sylius 1.7 only, skip this step for Sylius 1.6):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <h4class="ui top attached large header">{{ 'sylius.ui.general'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_errors(form) }}
    <divclass="fields">
    <divclass="six wide field">
    {{ form_row(form.code) }}
    </div>
    <divclass="seven wide field">
    {{ form_row(form.name) }}
    </div>
    <divclass="three wide field">
    {{ form_row(form.color) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    {{ form_row(form.enabled) }}
    </div>
    <divclass="ui attached segment">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    {{ form_row(form.description, {'attr': {'rows' : '3'}}) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.countries) }}
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.money'|trans }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.currencies) }}
    </div>
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    </div>
    </div>
    <divclass="column">
    <h4class="ui top attached large header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.look_and_feel'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_row(form.themeName) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.menuTaxon) }}
    </div>
    <divclass="ui hidden divider"></div>
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui attached segment">
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/Widget/_popup.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sylius_template_event('sylius.shop.cart.widget.button', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sylius_template_event('sylius.shop.cart.widget.popup', {'cart': cart}) }}
    </div>
  8. Install assets:

    bin/console sylius:install:assets

Setup development environment:

docker-compose build
docker-compose up -d
docker-compose exec php composer --working-dir=/srv/sylius install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application build
docker-compose exec php bin/console assets:install public
docker-compose exec php bin/console doctrine:schema:create
docker-compose exec php bin/console sylius:fixtures:load -n

Running tests:

docker-compose exec php sh
bin/console doc:sche:cre
cd ../..
vendor/bin/phpcs
vendor/bin/phpstan analyse src/ --level max
vendor/bin/phpunit
vendor/bin/behat

About

With this Sylius plugin a minimum order value can be set for each channel.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

build:started

This plugin adds the option of configuring a minimum order value for each channel.

When a minimum order value is configured it is not possible to checkout the order if the subtotal of all of the items in the cart is lower the configured minimum.

Configuration by administrator:

Configuring a minimum order value is possible by logging in as an administrator and navigating to the channel configuration:

admin_menu

Choose to edit an existing channel or to add a new one:

admin_channel_overview

This plugin adds a segment to the add/edit pane that can be used to set the desired minimum order value in the base currency:

admin_channel_details

By default the toggle is set to the off position and the field to enter the minimum order value is disabled. To configure the desired value use the toggle to enable the minimum order value and enter the value in the field.

What do visitors see?

When visiting the shop and not having added enough items to the shopping cart they will not be able to proceed to checkout:

shop_cart_summary

The checkout button is disabled and a message is displayed, providing more insight in the current status:

shop_minimum_order_value_message

When clicking the cart widget button, the checkout button is hidden:

shop_cart_widget

If a visitor tries to skip ahead to the checkout process (for example by entering the url manually), the visitor will automatically be redirected to the cart summary page.

How does it work?

Technically what this plugin does is a few things. It adds an optional field to the channel entity and the option to configure the fields value through the admin system.

This value is used by the MinimumOrderValueReached class, which is configured as a guard on all of the transitions of the "sylius_order_checkout" state machine.

By overriding the "sylius.resolver.checkout" service and disabling the original service, the plugin is able to redirect the visitor to the cart summary page when the guard blocks the transition.

Overriding that service also means that the service needs to be configured slightly differently then before (only applicable if you have a customised checkout resolver configuration):

Whereas before the the checkout resolver could be configured through key "sylius_shop", it now needs to be done through key "nedac_sylius_minimum_order_value":

nedac_sylius_minimum_order_value:
checkout_resolver:
pattern: /checkout/.+route_map:
empty_order:
route: sylius_shop_cart_summarycart:
route: sylius_shop_checkout_addressaddressed:
route: sylius_shop_checkout_select_shippingshipping_selected:
route: sylius_shop_checkout_select_paymentshipping_skipped:
route: sylius_shop_checkout_select_paymentpayment_selected:
route: sylius_shop_checkout_completepayment_skipped:
route: sylius_shop_checkout_complete

This is the default configuration added by this plugin in src/Resources/config/config.yaml. It can be overridden on an application level as with any Symfony bundle configuration.

Please see the official Sylius docs on how to configure the checkout resolver, should there be any need to do so.

Supported Sylius versions:
1.6
1.7

NOTE:This plugin requires PHP 7.4 or up

Installation:

  1. Install using composer:

    composer require nedac/sylius-minimum-order-value-plugin
  2. Add to bundles.php:

    # config/bundles.php<?phpreturn [
    # ...Nedac\SyliusMinimumOrderValuePlugin\NedacSyliusMinimumOrderValuePlugin::class => ['all' => true],
    ];
  3. Import configuration file:

    # config/packages/_sylius.yamlimports:
    # ...
    - { resource: "@NedacSyliusMinimumOrderValuePlugin/Resources/config/config.yaml" }# ...
  4. Implement the interface and use the trait in the Channel model:

    # src/entity/Channel/Channel.php<?phpdeclare(strict_types=1);
    namespaceApp\Entity\Channel;
    useDoctrine\ORM\MappingasORM;
    useNedac\SyliusMinimumOrderValuePlugin\Model\ChannelInterfaceasNedacSyliusMinimumOrderValuePluginChannelInterface;
    useNedac\SyliusMinimumOrderValuePlugin\Model\MinimumOrderValueTraitasNedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    useSylius\Component\Core\Model\ChannelasBaseChannel;
    /** * @ORM\Entity * @ORM\Table(name="sylius_channel") */class Channel extends BaseChannel implements NedacSyliusMinimumOrderValuePluginChannelInterface
    {
    use NedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    // ...
    }

    If the file src/entity/Channel/Channel.php does not yet exit, please create it using this example. When you do, also add the following section to config/packages/_sylius.yaml:

    sylius_channel:
    resources:
    channel:
    classes:
    model: App\Entity\Channel\Channel
  5. Generate and run database migration:

    bin/console doctrine:migrations:diff
    bin/console doctrine:migrations:migrate
  6. Override templates (For Sylius 1.7 skip this step):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <divclass="ui segment">
    {{ form_errors(form) }}
    <divclass="two fields">
    {{ form_row(form.code) }}
    {{ form_row(form.name) }}
    </div>
    {{ form_row(form.description) }}
    {{ form_row(form.enabled) }}
    <divclass="two fields">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.color) }}
    {{ form_row(form.themeName) }}
    </div>
    </div>
    <divclass="ui segment">
    <h4class="ui dividing header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    </div>
    <divclass="column">
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    {{ form_row(form.currencies) }}
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/_widget.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_widget_content', {'cart': cart}) }}
    <iclass="cart icon"></i>
    <spanid="sylius-cart-total">
    {{ money.convertAndFormat(cart.itemsTotal) }}
    </span>
    {% transchoicecart.items|length %}sylius.ui.item.choice{% endtranschoice %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_widget_content', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_popup_content', {'cart': cart}) }}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_popup_content', {'cart': cart}) }}
    </div>
  7. Override templates (Sylius 1.7 only, skip this step for Sylius 1.6):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <h4class="ui top attached large header">{{ 'sylius.ui.general'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_errors(form) }}
    <divclass="fields">
    <divclass="six wide field">
    {{ form_row(form.code) }}
    </div>
    <divclass="seven wide field">
    {{ form_row(form.name) }}
    </div>
    <divclass="three wide field">
    {{ form_row(form.color) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    {{ form_row(form.enabled) }}
    </div>
    <divclass="ui attached segment">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    {{ form_row(form.description, {'attr': {'rows' : '3'}}) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.countries) }}
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.money'|trans }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.currencies) }}
    </div>
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    </div>
    </div>
    <divclass="column">
    <h4class="ui top attached large header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.look_and_feel'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_row(form.themeName) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.menuTaxon) }}
    </div>
    <divclass="ui hidden divider"></div>
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui attached segment">
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/Widget/_popup.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sylius_template_event('sylius.shop.cart.widget.button', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sylius_template_event('sylius.shop.cart.widget.popup', {'cart': cart}) }}
    </div>
  8. Install assets:

    bin/console sylius:install:assets

Setup development environment:

docker-compose build
docker-compose up -d
docker-compose exec php composer --working-dir=/srv/sylius install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application build
docker-compose exec php bin/console assets:install public
docker-compose exec php bin/console doctrine:schema:create
docker-compose exec php bin/console sylius:fixtures:load -n

Running tests:

docker-compose exec php sh
bin/console doc:sche:cre
cd ../..
vendor/bin/phpcs
vendor/bin/phpstan analyse src/ --level max
vendor/bin/phpunit
vendor/bin/behat

About

With this Sylius plugin a minimum order value can be set for each channel.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

build:started

This plugin adds the option of configuring a minimum order value for each channel.

When a minimum order value is configured it is not possible to checkout the order if the subtotal of all of the items in the cart is lower the configured minimum.

Configuration by administrator:

Configuring a minimum order value is possible by logging in as an administrator and navigating to the channel configuration:

admin_menu

Choose to edit an existing channel or to add a new one:

admin_channel_overview

This plugin adds a segment to the add/edit pane that can be used to set the desired minimum order value in the base currency:

admin_channel_details

By default the toggle is set to the off position and the field to enter the minimum order value is disabled. To configure the desired value use the toggle to enable the minimum order value and enter the value in the field.

What do visitors see?

When visiting the shop and not having added enough items to the shopping cart they will not be able to proceed to checkout:

shop_cart_summary

The checkout button is disabled and a message is displayed, providing more insight in the current status:

shop_minimum_order_value_message

When clicking the cart widget button, the checkout button is hidden:

shop_cart_widget

If a visitor tries to skip ahead to the checkout process (for example by entering the url manually), the visitor will automatically be redirected to the cart summary page.

How does it work?

Technically what this plugin does is a few things. It adds an optional field to the channel entity and the option to configure the fields value through the admin system.

This value is used by the MinimumOrderValueReached class, which is configured as a guard on all of the transitions of the "sylius_order_checkout" state machine.

By overriding the "sylius.resolver.checkout" service and disabling the original service, the plugin is able to redirect the visitor to the cart summary page when the guard blocks the transition.

Overriding that service also means that the service needs to be configured slightly differently then before (only applicable if you have a customised checkout resolver configuration):

Whereas before the the checkout resolver could be configured through key "sylius_shop", it now needs to be done through key "nedac_sylius_minimum_order_value":

nedac_sylius_minimum_order_value:
checkout_resolver:
pattern: /checkout/.+route_map:
empty_order:
route: sylius_shop_cart_summarycart:
route: sylius_shop_checkout_addressaddressed:
route: sylius_shop_checkout_select_shippingshipping_selected:
route: sylius_shop_checkout_select_paymentshipping_skipped:
route: sylius_shop_checkout_select_paymentpayment_selected:
route: sylius_shop_checkout_completepayment_skipped:
route: sylius_shop_checkout_complete

This is the default configuration added by this plugin in src/Resources/config/config.yaml. It can be overridden on an application level as with any Symfony bundle configuration.

Please see the official Sylius docs on how to configure the checkout resolver, should there be any need to do so.

Supported Sylius versions:
1.6
1.7

NOTE:This plugin requires PHP 7.4 or up

Installation:

  1. Install using composer:

    composer require nedac/sylius-minimum-order-value-plugin
  2. Add to bundles.php:

    # config/bundles.php<?phpreturn [
    # ...Nedac\SyliusMinimumOrderValuePlugin\NedacSyliusMinimumOrderValuePlugin::class => ['all' => true],
    ];
  3. Import configuration file:

    # config/packages/_sylius.yamlimports:
    # ...
    - { resource: "@NedacSyliusMinimumOrderValuePlugin/Resources/config/config.yaml" }# ...
  4. Implement the interface and use the trait in the Channel model:

    # src/entity/Channel/Channel.php<?phpdeclare(strict_types=1);
    namespaceApp\Entity\Channel;
    useDoctrine\ORM\MappingasORM;
    useNedac\SyliusMinimumOrderValuePlugin\Model\ChannelInterfaceasNedacSyliusMinimumOrderValuePluginChannelInterface;
    useNedac\SyliusMinimumOrderValuePlugin\Model\MinimumOrderValueTraitasNedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    useSylius\Component\Core\Model\ChannelasBaseChannel;
    /** * @ORM\Entity * @ORM\Table(name="sylius_channel") */class Channel extends BaseChannel implements NedacSyliusMinimumOrderValuePluginChannelInterface
    {
    use NedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    // ...
    }

    If the file src/entity/Channel/Channel.php does not yet exit, please create it using this example. When you do, also add the following section to config/packages/_sylius.yaml:

    sylius_channel:
    resources:
    channel:
    classes:
    model: App\Entity\Channel\Channel
  5. Generate and run database migration:

    bin/console doctrine:migrations:diff
    bin/console doctrine:migrations:migrate
  6. Override templates (For Sylius 1.7 skip this step):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <divclass="ui segment">
    {{ form_errors(form) }}
    <divclass="two fields">
    {{ form_row(form.code) }}
    {{ form_row(form.name) }}
    </div>
    {{ form_row(form.description) }}
    {{ form_row(form.enabled) }}
    <divclass="two fields">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.color) }}
    {{ form_row(form.themeName) }}
    </div>
    </div>
    <divclass="ui segment">
    <h4class="ui dividing header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    </div>
    <divclass="column">
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    {{ form_row(form.currencies) }}
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/_widget.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_widget_content', {'cart': cart}) }}
    <iclass="cart icon"></i>
    <spanid="sylius-cart-total">
    {{ money.convertAndFormat(cart.itemsTotal) }}
    </span>
    {% transchoicecart.items|length %}sylius.ui.item.choice{% endtranschoice %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_widget_content', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_popup_content', {'cart': cart}) }}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_popup_content', {'cart': cart}) }}
    </div>
  7. Override templates (Sylius 1.7 only, skip this step for Sylius 1.6):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <h4class="ui top attached large header">{{ 'sylius.ui.general'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_errors(form) }}
    <divclass="fields">
    <divclass="six wide field">
    {{ form_row(form.code) }}
    </div>
    <divclass="seven wide field">
    {{ form_row(form.name) }}
    </div>
    <divclass="three wide field">
    {{ form_row(form.color) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    {{ form_row(form.enabled) }}
    </div>
    <divclass="ui attached segment">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    {{ form_row(form.description, {'attr': {'rows' : '3'}}) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.countries) }}
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.money'|trans }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.currencies) }}
    </div>
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    </div>
    </div>
    <divclass="column">
    <h4class="ui top attached large header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.look_and_feel'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_row(form.themeName) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.menuTaxon) }}
    </div>
    <divclass="ui hidden divider"></div>
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui attached segment">
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/Widget/_popup.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sylius_template_event('sylius.shop.cart.widget.button', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sylius_template_event('sylius.shop.cart.widget.popup', {'cart': cart}) }}
    </div>
  8. Install assets:

    bin/console sylius:install:assets

Setup development environment:

docker-compose build
docker-compose up -d
docker-compose exec php composer --working-dir=/srv/sylius install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application build
docker-compose exec php bin/console assets:install public
docker-compose exec php bin/console doctrine:schema:create
docker-compose exec php bin/console sylius:fixtures:load -n

Running tests:

docker-compose exec php sh
bin/console doc:sche:cre
cd ../..
vendor/bin/phpcs
vendor/bin/phpstan analyse src/ --level max
vendor/bin/phpunit
vendor/bin/behat

About

With this Sylius plugin a minimum order value can be set for each channel.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

build:started

This plugin adds the option of configuring a minimum order value for each channel.

When a minimum order value is configured it is not possible to checkout the order if the subtotal of all of the items in the cart is lower the configured minimum.

Configuration by administrator:

Configuring a minimum order value is possible by logging in as an administrator and navigating to the channel configuration:

admin_menu

Choose to edit an existing channel or to add a new one:

admin_channel_overview

This plugin adds a segment to the add/edit pane that can be used to set the desired minimum order value in the base currency:

admin_channel_details

By default the toggle is set to the off position and the field to enter the minimum order value is disabled. To configure the desired value use the toggle to enable the minimum order value and enter the value in the field.

What do visitors see?

When visiting the shop and not having added enough items to the shopping cart they will not be able to proceed to checkout:

shop_cart_summary

The checkout button is disabled and a message is displayed, providing more insight in the current status:

shop_minimum_order_value_message

When clicking the cart widget button, the checkout button is hidden:

shop_cart_widget

If a visitor tries to skip ahead to the checkout process (for example by entering the url manually), the visitor will automatically be redirected to the cart summary page.

How does it work?

Technically what this plugin does is a few things. It adds an optional field to the channel entity and the option to configure the fields value through the admin system.

This value is used by the MinimumOrderValueReached class, which is configured as a guard on all of the transitions of the "sylius_order_checkout" state machine.

By overriding the "sylius.resolver.checkout" service and disabling the original service, the plugin is able to redirect the visitor to the cart summary page when the guard blocks the transition.

Overriding that service also means that the service needs to be configured slightly differently then before (only applicable if you have a customised checkout resolver configuration):

Whereas before the the checkout resolver could be configured through key "sylius_shop", it now needs to be done through key "nedac_sylius_minimum_order_value":

nedac_sylius_minimum_order_value:
checkout_resolver:
pattern: /checkout/.+route_map:
empty_order:
route: sylius_shop_cart_summarycart:
route: sylius_shop_checkout_addressaddressed:
route: sylius_shop_checkout_select_shippingshipping_selected:
route: sylius_shop_checkout_select_paymentshipping_skipped:
route: sylius_shop_checkout_select_paymentpayment_selected:
route: sylius_shop_checkout_completepayment_skipped:
route: sylius_shop_checkout_complete

This is the default configuration added by this plugin in src/Resources/config/config.yaml. It can be overridden on an application level as with any Symfony bundle configuration.

Please see the official Sylius docs on how to configure the checkout resolver, should there be any need to do so.

Supported Sylius versions:
1.6
1.7

NOTE:This plugin requires PHP 7.4 or up

Installation:

  1. Install using composer:

    composer require nedac/sylius-minimum-order-value-plugin
  2. Add to bundles.php:

    # config/bundles.php<?phpreturn [
    # ...Nedac\SyliusMinimumOrderValuePlugin\NedacSyliusMinimumOrderValuePlugin::class => ['all' => true],
    ];
  3. Import configuration file:

    # config/packages/_sylius.yamlimports:
    # ...
    - { resource: "@NedacSyliusMinimumOrderValuePlugin/Resources/config/config.yaml" }# ...
  4. Implement the interface and use the trait in the Channel model:

    # src/entity/Channel/Channel.php<?phpdeclare(strict_types=1);
    namespaceApp\Entity\Channel;
    useDoctrine\ORM\MappingasORM;
    useNedac\SyliusMinimumOrderValuePlugin\Model\ChannelInterfaceasNedacSyliusMinimumOrderValuePluginChannelInterface;
    useNedac\SyliusMinimumOrderValuePlugin\Model\MinimumOrderValueTraitasNedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    useSylius\Component\Core\Model\ChannelasBaseChannel;
    /** * @ORM\Entity * @ORM\Table(name="sylius_channel") */class Channel extends BaseChannel implements NedacSyliusMinimumOrderValuePluginChannelInterface
    {
    use NedacSyliusMinimumOrderValuePluginMinimumOrderValueTrait;
    // ...
    }

    If the file src/entity/Channel/Channel.php does not yet exit, please create it using this example. When you do, also add the following section to config/packages/_sylius.yaml:

    sylius_channel:
    resources:
    channel:
    classes:
    model: App\Entity\Channel\Channel
  5. Generate and run database migration:

    bin/console doctrine:migrations:diff
    bin/console doctrine:migrations:migrate
  6. Override templates (For Sylius 1.7 skip this step):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <divclass="ui segment">
    {{ form_errors(form) }}
    <divclass="two fields">
    {{ form_row(form.code) }}
    {{ form_row(form.name) }}
    </div>
    {{ form_row(form.description) }}
    {{ form_row(form.enabled) }}
    <divclass="two fields">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.color) }}
    {{ form_row(form.themeName) }}
    </div>
    </div>
    <divclass="ui segment">
    <h4class="ui dividing header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    </div>
    <divclass="column">
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    {{ form_row(form.currencies) }}
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/_widget.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_widget_content', {'cart': cart}) }}
    <iclass="cart icon"></i>
    <spanid="sylius-cart-total">
    {{ money.convertAndFormat(cart.itemsTotal) }}
    </span>
    {% transchoicecart.items|length %}sylius.ui.item.choice{% endtranschoice %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_widget_content', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.before_popup_content', {'cart': cart}) }}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    {{ sonata_block_render_event('sylius.shop.partial.cart.summary.after_popup_content', {'cart': cart}) }}
    </div>
  7. Override templates (Sylius 1.7 only, skip this step for Sylius 1.6):

    {# templates/bundles/SyliusAdminBundle/Channel/_form.html.twig #}
    {{ form_errors(form) }}
    <divclass="ui two column stackable grid">
    <divclass="column">
    <h4class="ui top attached large header">{{ 'sylius.ui.general'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_errors(form) }}
    <divclass="fields">
    <divclass="six wide field">
    {{ form_row(form.code) }}
    </div>
    <divclass="seven wide field">
    {{ form_row(form.name) }}
    </div>
    <divclass="three wide field">
    {{ form_row(form.color) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    {{ form_row(form.enabled) }}
    </div>
    <divclass="ui attached segment">
    <divclass="field">
    {{ form_label(form.hostname) }}
    <divclass="ui labeled input">
    <divclass="ui label">http://</div>
    {{ form_widget(form.hostname) }}
    </div>
    {{ form_errors(form.hostname) }}
    </div>
    {{ form_row(form.contactEmail) }}
    {{ form_row(form.description, {'attr': {'rows' : '3'}}) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.countries) }}
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.money'|trans }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.baseCurrency) }}
    {{ form_row(form.currencies) }}
    </div>
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.defaultTaxZone) }}
    {{ form_row(form.taxCalculationStrategy) }}
    </div>
    </div>
    <divclass="column">
    <h4class="ui top attached large header">{{ form_label(form.shopBillingData) }}</h4>
    <divclass="ui attached segment">
    <divclass="two fields">
    {{ form_row(form.shopBillingData.company) }}
    {{ form_row(form.shopBillingData.taxId) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.countryCode) }}
    {{ form_row(form.shopBillingData.street) }}
    </div>
    <divclass="two fields">
    {{ form_row(form.shopBillingData.city) }}
    {{ form_row(form.shopBillingData.postcode) }}
    </div>
    </div>
    <divclass="ui hidden divider"></div>
    <h4class="ui top attached large header">{{ 'sylius.ui.look_and_feel'|trans }}</h4>
    <divclass="ui attached segment">
    {{ form_row(form.themeName) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.locales) }}
    {{ form_row(form.defaultLocale) }}
    </div>
    <divclass="ui attached segment">
    {{ form_row(form.menuTaxon) }}
    </div>
    <divclass="ui hidden divider"></div>
    <divid="nedac-sylius-minimum-order-value-plugin-admin-before"class="ui attached segment">
    {{ form_row(form.skippingShippingStepAllowed) }}
    {{ form_row(form.skippingPaymentStepAllowed) }}
    {{ form_row(form.accountVerificationRequired) }}
    </div>
    {% ifform.minimumOrderValueisdefined %}
    <divid="nedac-sylius-minimum-order-value-plugin-admin-segment"class="ui segment">
    {{ form_row(form.minimumOrderValue) }}
    <divclass="ui toggle checkbox">
    <inputid="nedac-sylius-minimum-order-value-plugin-admin-toggle"type="checkbox"name="public">
    <label>{{ 'nedac_sylius_minimum_order_value_plugin.ui.enabled'|trans }}</label>
    </div>
    </div>
    {% endif %}
    </div>
    </div>
    {# templates/bundles/SyliusShopBundle/Cart/Summary/_checkout.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% iftotal>=minimumOrderValue %}
    {% setbuttonClass="ui huge primary fluid labeled icon button" %}
    {% else %}
    {% setbuttonClass="ui huge primary fluid labeled icon button disabled" %}
    {% setformattedMinimum= money.convertAndFormat(minimumOrderValue) %}
    {% setformattedDifference= money.convertAndFormat(minimumOrderValue-total) %}
    <divclass="ui icon negative message">
    <iclass="warning icon"></i>
    <divclass="content">
    <divclass="header">{{ 'nedac_sylius_minimum_order_value_plugin.ui.attention'|trans }}</div>
    <pid="nedac-sylius-minimum-order-value-plugin-message">{{ 'nedac_sylius_minimum_order_value_plugin.ui.minimum_not_yet_reached'|trans({ '%minimumOrderValue% ': formattedMinimum, '%difference%': formattedDifference })|raw('br') }}</p>
    </div>
    </div>
    {% endif %}
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="{{ buttonClass }}"id="nedac-checkout-button"><iclass="check icon"></i> {{ 'sylius.ui.checkout'|trans }}</a>
    {# templates/bundles/SyliusShopBundle/Cart/Widget/_popup.html.twig #}
    {% import"@SyliusShop/Common/Macro/money.html.twig"asmoney %}
    {% setminimumOrderValue=cart.channel.minimumOrderValue %}
    {% settotal= sylius_order_items_subtotal(cart) %}
    {% ifcart.empty %}
    {{ 'sylius.ui.your_cart_is_empty'|trans }}.
    {% else %}
    <divclass="ui list">
    {% foritemincart.items %}
    <divclass="item">{{ item.quantity }} x <strong>{{ item.product }}</strong> {{ money.convertAndFormat(item.unitPrice) }}</div>
    {% endfor %}
    <divclass="item"><strong>{{ 'sylius.ui.subtotal'|trans }}</strong>: {{ money.convertAndFormat(cart.itemsTotal) }}</div>
    </div>
    <ahref="{{ path('sylius_shop_cart_summary') }}"class="ui fluid basic text button">{{ 'sylius.ui.view_and_edit_cart'|trans }}</a>
    {% iftotal>=minimumOrderValue %}
    <divclass="ui divider"></div>
    <ahref="{{ path('sylius_shop_checkout_start') }}"class="ui fluid primary button">{{ 'sylius.ui.checkout'|trans }}</a>
    {% endif %}
    {% endif %}
    <divid="sylius-cart-button"class="ui circular cart button">
    {{ sylius_template_event('sylius.shop.cart.widget.button', {'cart': cart}) }}
    </div>
    <divclass="ui large flowing cart hidden popup"id="nedac-sylius-minimum-order-value-plugin-popup">
    {{ sylius_template_event('sylius.shop.cart.widget.popup', {'cart': cart}) }}
    </div>
  8. Install assets:

    bin/console sylius:install:assets

Setup development environment:

docker-compose build
docker-compose up -d
docker-compose exec php composer --working-dir=/srv/sylius install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application install
docker-compose run --rm nodejs yarn --cwd=/srv/sylius/tests/Application build
docker-compose exec php bin/console assets:install public
docker-compose exec php bin/console doctrine:schema:create
docker-compose exec php bin/console sylius:fixtures:load -n

Running tests:

docker-compose exec php sh
bin/console doc:sche:cre
cd ../..
vendor/bin/phpcs
vendor/bin/phpstan analyse src/ --level max
vendor/bin/phpunit
vendor/bin/behat

About

With this Sylius plugin a minimum order value can be set for each channel.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages