From f5e51a28b6a832443780ed1f100cc52b441ed37e Mon Sep 17 00:00:00 2001 From: Thomas Klein Date: Sun, 10 May 2020 12:40:50 +0200 Subject: [PATCH] v1 --- Api/CanShowPriceInterface.php | 16 ++ Api/IsSaleableInterface.php | 16 ++ LICENSE | 21 ++ Model/CanShowPrice.php | 67 ++++++ Model/IsSaleable.php | 76 +++++++ Observer/Product/IsSaleable.php | 50 +++++ Plugin/Pricing/CanShowPrice.php | 41 ++++ Plugin/Pricing/Renderer/CanShowPrice.php | 41 ++++ README.md | 81 +++++++ Test/Unit/Model/TestCanShowPrice.php | 62 ++++++ Test/Unit/Model/TestIsSaleable.php | 257 +++++++++++++++++++++++ composer.json | 56 +++++ etc/adminhtml/system.xml | 49 +++++ etc/config.xml | 21 ++ etc/di.xml | 17 ++ etc/events.xml | 12 ++ etc/module.xml | 18 ++ registration.php | 10 + 18 files changed, 911 insertions(+) create mode 100644 Api/CanShowPriceInterface.php create mode 100644 Api/IsSaleableInterface.php create mode 100644 LICENSE create mode 100644 Model/CanShowPrice.php create mode 100644 Model/IsSaleable.php create mode 100644 Observer/Product/IsSaleable.php create mode 100644 Plugin/Pricing/CanShowPrice.php create mode 100644 Plugin/Pricing/Renderer/CanShowPrice.php create mode 100644 README.md create mode 100644 Test/Unit/Model/TestCanShowPrice.php create mode 100644 Test/Unit/Model/TestIsSaleable.php create mode 100644 composer.json create mode 100644 etc/adminhtml/system.xml create mode 100644 etc/config.xml create mode 100644 etc/di.xml create mode 100644 etc/events.xml create mode 100644 etc/module.xml create mode 100644 registration.php diff --git a/Api/CanShowPriceInterface.php b/Api/CanShowPriceInterface.php new file mode 100644 index 0000000..044d294 --- /dev/null +++ b/Api/CanShowPriceInterface.php @@ -0,0 +1,16 @@ +scopeConfig = $scopeConfig; + } + + public function canShowPrice(int $customerGroupId): bool + { + return $this->isEnabled() ? in_array($customerGroupId, $this->resolveAllowedGroups(), true) : true; + } + + private function isEnabled(): bool + { + return $this->isEnabled ?? $this->isEnabled = $this->scopeConfig->isSetFlag( + self::CONFIG_PATH_RESTRICT_SHOW_PRICE, + ScopeInterface::SCOPE_WEBSITE + ); + } + + private function resolveAllowedGroups(): array + { + return $this->allowedGroups + ?? $this->allowedGroups = array_map('\intval', array_filter( + explode(',', (string) $this->scopeConfig->getValue( + self::CONFIG_PATH_CAN_SHOW_PRICE_GROUPS, + ScopeInterface::SCOPE_WEBSITE + )) + )); + } +} diff --git a/Model/IsSaleable.php b/Model/IsSaleable.php new file mode 100644 index 0000000..bb30e94 --- /dev/null +++ b/Model/IsSaleable.php @@ -0,0 +1,76 @@ +scopeConfig = $scopeConfig; + $this->canShowPrice = $canShowPrice; + } + + public function isSaleable(int $customerGroupId): bool + { + return $this->canShowPrice->canShowPrice($customerGroupId) + && (!$this->isEnabled() || in_array($customerGroupId, $this->resolveAllowedGroups(), true)); + } + + private function isEnabled(): bool + { + return $this->isEnabled ?? $this->isEnabled = $this->scopeConfig->isSetFlag( + self::CONFIG_PATH_RESTRICT_SALEABLE, + ScopeInterface::SCOPE_WEBSITE + ); + } + + private function resolveAllowedGroups(): array + { + return $this->allowedGroups + ?? $this->allowedGroups = array_map('\intval', array_filter( + explode(',', (string) $this->scopeConfig->getValue( + self::CONFIG_PATH_IS_SALEABLE_GROUPS, + ScopeInterface::SCOPE_WEBSITE + )) + )); + } +} diff --git a/Observer/Product/IsSaleable.php b/Observer/Product/IsSaleable.php new file mode 100644 index 0000000..134a9c2 --- /dev/null +++ b/Observer/Product/IsSaleable.php @@ -0,0 +1,50 @@ +httpContext = $httpContext; + $this->isSaleable = $isSaleable; + } + + public function execute(Observer $observer): void + { + $saleable = $observer->getData('salable'); + + if ($saleable instanceof DataObject) { + $saleable->setData( + 'is_salable', + (bool) $saleable->getData('is_salable') + ? $this->isSaleable->isSaleable((int) $this->httpContext->getValue(CustomerContext::CONTEXT_GROUP)) + : false + ); + } + } +} diff --git a/Plugin/Pricing/CanShowPrice.php b/Plugin/Pricing/CanShowPrice.php new file mode 100644 index 0000000..4405675 --- /dev/null +++ b/Plugin/Pricing/CanShowPrice.php @@ -0,0 +1,41 @@ +httpContext = $httpContext; + $this->canShowPrice = $canShowPrice; + } + + public function afterGetCanShowPrice(SaleableInterface $saleable, bool $canShowPrice): bool + { + return $canShowPrice + ? $this->canShowPrice->canShowPrice((int) $this->httpContext->getValue(CustomerContext::CONTEXT_GROUP)) + : false; + } +} diff --git a/Plugin/Pricing/Renderer/CanShowPrice.php b/Plugin/Pricing/Renderer/CanShowPrice.php new file mode 100644 index 0000000..48cd368 --- /dev/null +++ b/Plugin/Pricing/Renderer/CanShowPrice.php @@ -0,0 +1,41 @@ +httpContext = $httpContext; + $this->canShowPrice = $canShowPrice; + } + + public function afterIsSalable(SalableResolverInterface $salableResolver, bool $isSalable): bool + { + return $isSalable + ? $this->canShowPrice->canShowPrice((int) $this->httpContext->getValue(CustomerContext::CONTEXT_GROUP)) + : false; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..967a4bd --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# Saleable Module for Magento 2 + +[![Latest Stable Version](https://img.shields.io/packagist/v/opengento/module-saleable.svg?style=flat-square)](https://packagist.org/packages/opengento/module-saleable) +[![License: MIT](https://img.shields.io/github/license/opengento/magento2-saleable.svg?style=flat-square)](./LICENSE) +[![Packagist](https://img.shields.io/packagist/dt/opengento/module-saleable.svg?style=flat-square)](https://packagist.org/packages/opengento/module-saleable/stats) +[![Packagist](https://img.shields.io/packagist/dm/opengento/module-saleable.svg?style=flat-square)](https://packagist.org/packages/opengento/module-saleable/stats) + +This extension allows to set if a product is saleable and can show its price by scope and customer group. + + - [Setup](#setup) + - [Composer installation](#composer-installation) + - [Setup the module](#setup-the-module) + - [Features](#features) + - [Settings](#settings) + - [Support](#support) + - [Authors](#authors) + - [License](#license) + +## Setup + +Magento 2 Open Source or Commerce edition is required. + +### Composer installation + +Run the following composer command: + +``` +composer require opengento/module-saleable +``` + +### Setup the module + +Run the following magento command: + +``` +bin/magento setup:upgrade +``` + +**If you are in production mode, do not forget to recompile and redeploy the static resources.** + +## Features + +### Saleable + +- Define if the price can be displayed on the storefront, depending of the customer group and by scope. +- Define if the sales are enabled on the website and by customer groups. + +## Settings + +The configuration for this module is available in 'Stores > Configuration > Catalog > Catalog > Price'. + +- Show Prices for Customer Groups + +The configuration for this module is available in 'Stores > Configuration > Sales > Checkout > Shopping Cart'. + +- Enable Sales for Customer Groups + +### Warning + +If you need to determine the rules by products, do not use this module, instead create new product attributes: + +- can_show_price (boolean) +- salable (boolean) + +Magento will automatically handle these attributes to check if a product is saleable or its price can be displayed. + +## Support + +Raise a new [request](https://github.com/opengento/magento2-saleable/issues) to the issue tracker. + +## Authors + +- **Opengento Community** - *Lead* - [![Twitter Follow](https://img.shields.io/twitter/follow/opengento.svg?style=social)](https://twitter.com/opengento) +- **Thomas Klein** - *Maintainer* - [![GitHub followers](https://img.shields.io/github/followers/thomas-kl1.svg?style=social)](https://github.com/thomas-kl1) +- **Contributors** - *Contributor* - [![GitHub contributors](https://img.shields.io/github/contributors/opengento/magento2-saleable.svg?style=flat-square)](https://github.com/opengento/magento2-saleable/graphs/contributors) + +## License + +This project is licensed under the MIT License - see the [LICENSE](./LICENSE) details. + +***That's all folks!*** diff --git a/Test/Unit/Model/TestCanShowPrice.php b/Test/Unit/Model/TestCanShowPrice.php new file mode 100644 index 0000000..1e0bd0b --- /dev/null +++ b/Test/Unit/Model/TestCanShowPrice.php @@ -0,0 +1,62 @@ +scopeConfig = $this->getMockForAbstractClass(ScopeConfigInterface::class); + + $this->canShowPrice = new CanShowPrice($this->scopeConfig); + } + + /** + * @dataProvider canShowPriceDataProvider + */ + public function testCanShowPrice(bool $isEnabled, ?string $config, array $expectations): void + { + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->with('catalog/price/restrict_show_price', 'website', null) + ->willReturn($isEnabled); + $this->scopeConfig->expects($isEnabled ? $this->once() : $this->never()) + ->method('getValue') + ->with('catalog/price/can_show_price_groups', 'website', null) + ->willReturn($config); + + foreach ($expectations as $groupId => $assert) { + $this->assertSame($assert, $this->canShowPrice->canShowPrice($groupId)); + } + } + + public function canShowPriceDataProvider(): array + { + return [ + [true, '1,2,3,4', [0 => false, 1 => true, 2 => true, 3 => true, 4 => true]], + [true, '1,3', [0 => false, 1 => true, 2 => false, 3 => true, 4 => false]], + [true, '', [0 => false, 1 => false, 2 => false, 3 => false, 4 => false]], + [false, null, [0 => true, 1 => true, 2 => true, 3 => true, 4 => true]], + ]; + } +} diff --git a/Test/Unit/Model/TestIsSaleable.php b/Test/Unit/Model/TestIsSaleable.php new file mode 100644 index 0000000..4e5f0f7 --- /dev/null +++ b/Test/Unit/Model/TestIsSaleable.php @@ -0,0 +1,257 @@ + '1,2,3,4', + self::CONFIG_SALEABLE_ENABLED_SOME => '2,3', + self::CONFIG_SALEABLE_ENABLED_NONE => '', + self::CONFIG_SALEABLE_DISABLED => null + ]; + + private const CONFIG_SALEABLE_EXPECTS = [ + self::CONFIG_SALEABLE_ENABLED_ALL => [ + self::CONFIG_SHOW_PRICE_ENABLED_ALL => [ + 0 => false, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + ], + self::CONFIG_SHOW_PRICE_ENABLED_SOME => [ + 0 => false, + 1 => false, + 2 => true, + 3 => false, + 4 => true, + ], + self::CONFIG_SHOW_PRICE_ENABLED_NONE => [ + 0 => false, + 1 => false, + 2 => false, + 3 => false, + 4 => false, + ], + self::CONFIG_SHOW_PRICE_DISABLED => [ + 0 => false, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + ], + ], + self::CONFIG_SALEABLE_ENABLED_SOME => [ + self::CONFIG_SHOW_PRICE_ENABLED_ALL => [ + 0 => false, + 1 => false, + 2 => true, + 3 => true, + 4 => false, + ], + self::CONFIG_SHOW_PRICE_ENABLED_SOME => [ + 0 => false, + 1 => false, + 2 => true, + 3 => false, + 4 => false, + ], + self::CONFIG_SHOW_PRICE_ENABLED_NONE => [ + 0 => false, + 1 => false, + 2 => false, + 3 => false, + 4 => false, + ], + self::CONFIG_SHOW_PRICE_DISABLED => [ + 0 => false, + 1 => false, + 2 => true, + 3 => true, + 4 => false, + ], + ], + self::CONFIG_SALEABLE_ENABLED_NONE => [ + self::CONFIG_SHOW_PRICE_ENABLED_ALL => [ + 0 => false, + 1 => false, + 2 => false, + 3 => false, + 4 => false, + ], + self::CONFIG_SHOW_PRICE_ENABLED_SOME => [ + 0 => false, + 1 => false, + 2 => false, + 3 => false, + 4 => false, + ], + self::CONFIG_SHOW_PRICE_ENABLED_NONE => [ + 0 => false, + 1 => false, + 2 => false, + 3 => false, + 4 => false, + ], + self::CONFIG_SHOW_PRICE_DISABLED => [ + 0 => false, + 1 => false, + 2 => false, + 3 => false, + 4 => false, + ], + ], + self::CONFIG_SALEABLE_DISABLED => [ + self::CONFIG_SHOW_PRICE_ENABLED_ALL => [ + 0 => false, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + ], + self::CONFIG_SHOW_PRICE_ENABLED_SOME => [ + 0 => false, + 1 => false, + 2 => true, + 3 => false, + 4 => true, + ], + self::CONFIG_SHOW_PRICE_ENABLED_NONE => [ + 0 => false, + 1 => false, + 2 => false, + 3 => false, + 4 => false, + ], + self::CONFIG_SHOW_PRICE_DISABLED => [ + 0 => true, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + ], + ] + ]; + + private const CONFIG_SHOW_PRICE_ENABLED_ALL = 1; + private const CONFIG_SHOW_PRICE_ENABLED_SOME = 2; + private const CONFIG_SHOW_PRICE_ENABLED_NONE = 3; + private const CONFIG_SHOW_PRICE_DISABLED = 4; + + private const CONFIG_SHOW_PRICE_MAP = [ + self::CONFIG_SHOW_PRICE_ENABLED_ALL => [ + [0, false], + [1, true], + [2, true], + [3, true], + [4, true], + ], + self::CONFIG_SHOW_PRICE_ENABLED_SOME => [ + [0, false], + [1, false], + [2, true], + [3, false], + [4, true], + ], + self::CONFIG_SHOW_PRICE_ENABLED_NONE => [ + [0, false], + [1, false], + [2, false], + [3, false], + [4, false], + ], + self::CONFIG_SHOW_PRICE_DISABLED => [ + [0, true], + [1, true], + [2, true], + [3, true], + [4, true], + ], + ]; + + /** + * @var MockObject|ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var MockObject|CanShowPriceInterface + */ + private $canShowPrice; + + /** + * @var IsSaleable + */ + private $isSaleable; + + protected function setUp() + { + $this->scopeConfig = $this->getMockForAbstractClass(ScopeConfigInterface::class); + $this->canShowPrice = $this->getMockForAbstractClass(CanShowPriceInterface::class); + + $this->isSaleable = new IsSaleable($this->scopeConfig, $this->canShowPrice); + } + + /** + * @dataProvider isSaleableDataProvider + */ + public function testIsSaleable(bool $isSaleableEnabled, int $saleableConfigStatus, int $showPriceMapStatus): void + { + $isSaleableExpected = $showPriceMapStatus === self::CONFIG_SHOW_PRICE_ENABLED_NONE + ? $this->never() + : $this->once(); + $this->scopeConfig->expects($isSaleableExpected) + ->method('isSetFlag') + ->with('checkout/cart/restrict_saleable', 'website', null) + ->willReturn($isSaleableEnabled); + $this->scopeConfig->expects($isSaleableEnabled ? $this->any() : $this->never()) + ->method('getValue') + ->with('checkout/cart/is_saleable_groups', 'website', null) + ->willReturn(self::CONFIG_SALEABLE_GROUPS[$saleableConfigStatus]); + $this->canShowPrice->method('canShowPrice') + ->willReturnMap(self::CONFIG_SHOW_PRICE_MAP[$showPriceMapStatus]); + + foreach (self::CONFIG_SALEABLE_EXPECTS[$saleableConfigStatus][$showPriceMapStatus] as $groupId => $assert) { + $this->assertSame($assert, $this->isSaleable->isSaleable($groupId)); + } + } + + public function isSaleableDataProvider(): array + { + return [ + [true, self::CONFIG_SALEABLE_ENABLED_ALL, self::CONFIG_SHOW_PRICE_ENABLED_ALL], + [true, self::CONFIG_SALEABLE_ENABLED_ALL, self::CONFIG_SHOW_PRICE_ENABLED_SOME], + [true, self::CONFIG_SALEABLE_ENABLED_ALL, self::CONFIG_SHOW_PRICE_ENABLED_NONE], + [true, self::CONFIG_SALEABLE_ENABLED_ALL, self::CONFIG_SHOW_PRICE_DISABLED], + [true, self::CONFIG_SALEABLE_ENABLED_SOME, self::CONFIG_SHOW_PRICE_ENABLED_ALL], + [true, self::CONFIG_SALEABLE_ENABLED_SOME, self::CONFIG_SHOW_PRICE_ENABLED_SOME], + [true, self::CONFIG_SALEABLE_ENABLED_SOME, self::CONFIG_SHOW_PRICE_ENABLED_NONE], + [true, self::CONFIG_SALEABLE_ENABLED_SOME, self::CONFIG_SHOW_PRICE_DISABLED], + [true, self::CONFIG_SALEABLE_ENABLED_NONE, self::CONFIG_SHOW_PRICE_ENABLED_ALL], + [true, self::CONFIG_SALEABLE_ENABLED_NONE, self::CONFIG_SHOW_PRICE_ENABLED_SOME], + [true, self::CONFIG_SALEABLE_ENABLED_NONE, self::CONFIG_SHOW_PRICE_ENABLED_NONE], + [true, self::CONFIG_SALEABLE_ENABLED_NONE, self::CONFIG_SHOW_PRICE_DISABLED], + [false, self::CONFIG_SALEABLE_DISABLED, self::CONFIG_SHOW_PRICE_ENABLED_ALL], + [false, self::CONFIG_SALEABLE_DISABLED, self::CONFIG_SHOW_PRICE_ENABLED_SOME], + [false, self::CONFIG_SALEABLE_DISABLED, self::CONFIG_SHOW_PRICE_ENABLED_NONE], + [false, self::CONFIG_SALEABLE_DISABLED, self::CONFIG_SHOW_PRICE_DISABLED], + ]; + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..61d5c88 --- /dev/null +++ b/composer.json @@ -0,0 +1,56 @@ +{ + "name": "opengento/module-saleable", + "description": "This extension allows to set if a product is saleable and can show its price by scope and customer group.", + "keywords": [ + "php", + "magento", + "magento2", + "saleable", + "pricing", + "customer", + "groups" + ], + "require": { + "php": "^7.1", + "magento/framework": "*", + "magento/module-catalog": "*", + "magento/module-checkout": "*", + "magento/module-config": "*", + "magento/module-customer": "*", + "magento/module-store": "*" + }, + "require-dev": { + "magento/marketplace-eqp": "^1.0.0" + }, + "type": "magento2-module", + "license": [ + "MIT" + ], + "homepage": "https://github.com/opengento/magento2-saleable", + "authors": [ + { + "name": "Opengento Team", + "email": "opengento@gmail.com", + "homepage": "https://opengento.fr/", + "role": "lead" + }, + { + "name": "Thomas Klein", + "email": "thomaskein876@gmail.com", + "homepage": "https://www.linkedin.com/in/thomas-klein/", + "role": "maintainer" + } + ], + "support": { + "source": "https://github.com/opengento/magento2-saleable", + "issues": "https://github.com/opengento/magento2-saleable/issues" + }, + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Opengento\\Saleable\\": "" + } + } +} diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml new file mode 100644 index 0000000..a481c3d --- /dev/null +++ b/etc/adminhtml/system.xml @@ -0,0 +1,49 @@ + + + + +
+ + + + Magento\Config\Model\Config\Source\Enabledisable + catalog/price/restrict_show_price + + + + Not logged in users will never been able to see prices. + Magento\Customer\Model\Config\Source\Group\Multiselect + 1 + + 1 + + catalog/price/can_show_price_groups + + +
+
+ + + + Magento\Config\Model\Config\Source\Enabledisable + checkout/cart/restrict_saleable + + + + Not logged in users will never been able to add to cart. + Magento\Customer\Model\Config\Source\Group\Multiselect + 1 + + 1 + + checkout/cart/is_saleable_groups + + +
+
+
diff --git a/etc/config.xml b/etc/config.xml new file mode 100644 index 0000000..4cc7dfb --- /dev/null +++ b/etc/config.xml @@ -0,0 +1,21 @@ + + + + + + + 0 + + + + + 0 + + + + diff --git a/etc/di.xml b/etc/di.xml new file mode 100644 index 0000000..1334ad6 --- /dev/null +++ b/etc/di.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/etc/events.xml b/etc/events.xml new file mode 100644 index 0000000..a4fc4bb --- /dev/null +++ b/etc/events.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/etc/module.xml b/etc/module.xml new file mode 100644 index 0000000..5b34096 --- /dev/null +++ b/etc/module.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/registration.php b/registration.php new file mode 100644 index 0000000..7bf90d1 --- /dev/null +++ b/registration.php @@ -0,0 +1,10 @@ +