Commit 109872c6 authored by bojanz's avatar bojanz

Issue #2885363 by bojanz: Create a commerce_condition plugin type, replacing...

Issue #2885363 by bojanz: Create a commerce_condition plugin type, replacing commerce_promotion_condition
parent a32d4444
......@@ -56,6 +56,10 @@ services:
tags:
- { name: cache.context}
plugin.manager.commerce_condition:
class: Drupal\commerce\ConditionManager
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@entity_type.manager']
plugin.manager.commerce_entity_trait:
class: Drupal\commerce\EntityTraitManager
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@commerce.configurable_field_manager']
......@@ -12,6 +12,12 @@ commerce_config_entity_bundle:
sequence:
type: string
commerce.commerce_condition.plugin.*:
type: commerce_condition_configuration
commerce_condition_configuration:
type: mapping
field.value.commerce_remote_id:
type: mapping
label: 'Default value'
......
commerce.commerce_condition.plugin.order_item_quantity:
type: commerce_condition_configuration
mapping:
operator:
type: string
label: 'Operator'
quantity:
type: integer
label: 'Quantity'
commerce.commerce_condition.plugin.commerce_promotion_order_total_price:
type: commerce_condition_configuration
mapping:
operator:
type: string
label: 'Operator'
amount:
type: field.value.commerce_price
label: 'Amount'
commerce_order.commerce_order_type.*:
type: commerce_config_entity_bundle
label: 'Order type'
......
<?php
namespace Drupal\commerce_order\Plugin\Commerce\Condition;
use Drupal\commerce\Plugin\Commerce\Condition\ConditionBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides the quantity condition for order items.
*
* @CommerceCondition(
* id = "order_item_quantity",
* label = @Translation("Quantity"),
* display_label = @Translation("Limit by quantity"),
* category = @Translation("Product"),
* entity_type = "commerce_order_item",
* )
*/
class OrderItemQuantity extends ConditionBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'operator' => '>',
'quantity' => 1,
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
$form['operator'] = [
'#type' => 'select',
'#title' => t('Operator'),
'#options' => $this->getComparisonOperators(),
'#default_value' => $this->configuration['operator'],
'#required' => TRUE,
];
$form['quantity'] = [
'#type' => 'number',
'#title' => t('Quantity'),
'#default_value' => $this->configuration['quantity'],
'#min' => 1,
'#required' => TRUE,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
$values = $form_state->getValue($form['#parents']);
$this->configuration['operator'] = $values['operator'];
$this->configuration['quantity'] = $values['quantity'];
}
/**
* {@inheritdoc}
*/
public function evaluate(EntityInterface $entity) {
$this->assertEntity($entity);
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
$order_item = $entity;
$quantity = $order_item->getQuantity();
switch ($this->configuration['operator']) {
case '>=':
return $quantity >= $this->configuration['quantity'];
case '>':
return $quantity > $this->configuration['quantity'];
case '<=':
return $quantity <= $this->configuration['quantity'];
case '<':
return $quantity < $this->configuration['quantity'];
case '==':
return $quantity == $this->configuration['quantity'];
default:
throw new \InvalidArgumentException("Invalid operator {$this->configuration['operator']}");
}
}
}
<?php
namespace Drupal\commerce_promotion\Plugin\Commerce\PromotionCondition;
namespace Drupal\commerce_order\Plugin\Commerce\Condition;
use Drupal\commerce\Plugin\Commerce\Condition\ConditionBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\commerce_price\Price;
/**
* Provides an 'Order: Total amount comparison' condition.
* Provides the total price condition for orders.
*
* @CommercePromotionCondition(
* @CommerceCondition(
* id = "commerce_promotion_order_total_price",
* label = @Translation("Total amount"),
* target_entity_type = "commerce_order",
* label = @Translation("Total price"),
* display_label = @Translation("Limit by total price"),
* category = @Translation("Order"),
* entity_type = "commerce_order",
* )
*/
class OrderTotalPrice extends PromotionConditionBase {
class OrderTotalPrice extends ConditionBase {
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'amount' => NULL,
'operator' => '>',
'amount' => NULL,
] + parent::defaultConfiguration();
}
......@@ -33,7 +37,7 @@ class OrderTotalPrice extends PromotionConditionBase {
$form = parent::buildConfigurationForm($form, $form_state);
$amount = $this->configuration['amount'];
// A bug in the plugin_select form element causes $amount to be incomplete.
// An #ajax bug can cause $amount to be incomplete.
if (isset($amount) && !isset($amount['number'], $amount['currency_code'])) {
$amount = NULL;
}
......@@ -69,44 +73,35 @@ class OrderTotalPrice extends PromotionConditionBase {
/**
* {@inheritdoc}
*/
public function evaluate() {
$amount = $this->configuration['amount'];
if (empty($amount)) {
return FALSE;
}
public function evaluate(EntityInterface $entity) {
$this->assertEntity($entity);
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $this->getTargetEntity();
/** @var \Drupal\commerce_price\Price $total_price */
$order = $entity;
$total_price = $order->getTotalPrice();
/** @var \Drupal\commerce_price\Price $comparison_price */
$comparison_price = new Price($amount['number'], $amount['currency_code']);
$condition_price = new Price($this->configuration['amount']['number'], $this->configuration['amount']['currency_code']);
if ($total_price->getCurrencyCode() != $condition_price->getCurrencyCode()) {
return FALSE;
}
switch ($this->configuration['operator']) {
case '>=':
return $total_price->greaterThanOrEqual($comparison_price);
return $total_price->greaterThanOrEqual($condition_price);
case '>':
return $total_price->greaterThan($comparison_price);
return $total_price->greaterThan($condition_price);
case '<=':
return $total_price->lessThanOrEqual($comparison_price);
return $total_price->lessThanOrEqual($condition_price);
case '<':
return $total_price->lessThan($comparison_price);
return $total_price->lessThan($condition_price);
case '==':
return $total_price->equals($comparison_price);
return $total_price->equals($condition_price);
default:
throw new \InvalidArgumentException("Invalid operator {$this->configuration['operator']}");
}
}
/**
* {@inheritdoc}
*/
public function summary() {
return $this->t('Compares the order total amount.');
}
}
<?php
namespace Drupal\Tests\commerce_order\Unit\Plugin\Commerce\Condition;
use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\commerce_order\Plugin\Commerce\Condition\OrderItemQuantity;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\commerce_order\Plugin\Commerce\Condition\OrderItemQuantity
* @group commerce
*/
class OrderItemQuantityTest extends UnitTestCase {
/**
* ::covers evaluate.
*
* @dataProvider quantityProvider
*/
public function testEvaluate($operator, $quantity, $given_quantity, $result) {
$condition = new OrderItemQuantity([
'operator' => $operator,
'quantity' => $quantity,
], 'order_item_quantity', ['entity_type' => 'commerce_order_item']);
$order_item = $this->prophesize(OrderItemInterface::class);
$order_item->getEntityTypeId()->willReturn('commerce_order_item');
$order_item->getQuantity()->willReturn($given_quantity);
$order_item = $order_item->reveal();
$this->assertEquals($result, $condition->evaluate($order_item));
}
/**
* Data provider for ::testEvaluate.
*
* @return array
* A list of testEvaluate function arguments.
*/
public function quantityProvider() {
return [
['>', 10, 5, FALSE],
['>', 10, 10, FALSE],
['>', 10, 11, TRUE],
['>=', 10, 5, FALSE],
['>=', 10, 10, TRUE],
['>=', 10, 11, TRUE],
['<', 10, 5, TRUE],
['<', 10, 10, FALSE],
['<', 10, 11, FALSE],
['<=', 10, 5, TRUE],
['<=', 10, 10, TRUE],
['<=', 10, 11, FALSE],
['==', 10, 5, FALSE],
['==', 10, 10, TRUE],
['==', 10, 11, FALSE],
];
}
}
<?php
namespace Drupal\Tests\commerce_order\Unit\Plugin\Commerce\Condition;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\commerce_order\Plugin\Commerce\Condition\OrderTotalPrice;
use Drupal\commerce_price\Price;
use Drupal\Tests\UnitTestCase;
/**
* @coversDefaultClass \Drupal\commerce_order\Plugin\Commerce\Condition\OrderTotalPrice
* @group commerce
*/
class OrderTotalPriceTest extends UnitTestCase {
/**
* ::covers evaluate.
*/
public function testMismatchedCurrencies() {
$condition = new OrderTotalPrice([
'operator' => '==',
'amount' => [
'number' => '10.00',
'currency_code' => 'EUR',
],
], 'order_total_price', ['entity_type' => 'commerce_order']);
$order = $this->prophesize(OrderInterface::class);
$order->getEntityTypeId()->willReturn('commerce_order');
$order->getTotalPrice()->willReturn(new Price('10.00', 'USD'));
$order = $order->reveal();
$this->assertEquals(FALSE, $condition->evaluate($order));
}
/**
* ::covers evaluate.
*
* @dataProvider totalPriceProvider
*/
public function testEvaluate($operator, $total_price, $given_total_price, $result) {
$condition = new OrderTotalPrice([
'operator' => $operator,
'amount' => [
'number' => $total_price,
'currency_code' => 'USD',
],
], 'order_total_price', ['entity_type' => 'commerce_order']);
$order = $this->prophesize(OrderInterface::class);
$order->getEntityTypeId()->willReturn('commerce_order');
$order->getTotalPrice()->willReturn(new Price($given_total_price, 'USD'));
$order = $order->reveal();
$this->assertEquals($result, $condition->evaluate($order));
}
/**
* Data provider for ::testEvaluate.
*
* @return array
* A list of testEvaluate function arguments.
*/
public function totalPriceProvider() {
return [
['>', 10, 5, FALSE],
['>', 10, 10, FALSE],
['>', 10, 11, TRUE],
['>=', 10, 5, FALSE],
['>=', 10, 10, TRUE],
['>=', 10, 11, TRUE],
['<', 10, 5, TRUE],
['<', 10, 10, FALSE],
['<', 10, 11, FALSE],
['<=', 10, 5, TRUE],
['<=', 10, 10, TRUE],
['<=', 10, 11, FALSE],
['==', 10, 5, FALSE],
['==', 10, 10, TRUE],
['==', 10, 11, FALSE],
];
}
}
......@@ -2,9 +2,6 @@ services:
plugin.manager.commerce_promotion_offer:
class: Drupal\commerce_promotion\PromotionOfferManager
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@entity_type.manager']
plugin.manager.commerce_promotion_condition:
class: Drupal\commerce_promotion\PromotionConditionManager
arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@entity_type.manager']
commerce_promotion.promotion_order_processor:
class: Drupal\commerce_promotion\PromotionOrderProcessor
......
<?php
namespace Drupal\commerce_promotion\Annotation;
use Drupal\Core\Condition\Annotation\Condition;
/**
* Defines the promotion condition plugin annotation object.
*
* Plugin namespace: Plugin\Commerce\PromotionCondition.
*
* @Annotation
*/
class CommercePromotionCondition extends Condition {
/**
* The target entity type this action applies to.
*
* For example, this should be 'commerce_order' or 'commerce_order_item'.
*
* @var string
*/
public $target_entity_type;
}
......@@ -2,6 +2,7 @@
namespace Drupal\commerce_promotion\Entity;
use Drupal\commerce\ConditionGroup;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\ContentEntityBase;
......@@ -166,6 +167,18 @@ class Promotion extends ContentEntityBase implements PromotionInterface {
return $this;
}
/**
* {@inheritdoc}
*/
public function getConditions() {
$conditions = [];
foreach ($this->get('conditions') as $field_item) {
/** @var \Drupal\commerce\Plugin\Field\FieldType\PluginItemInterface $field_item */
$conditions[] = $field_item->getTargetInstance();
}
return $conditions;
}
/**
* {@inheritdoc}
*/
......@@ -389,39 +402,29 @@ class Promotion extends ContentEntityBase implements PromotionInterface {
break;
}
// If there are no conditions, the promotion applies automatically.
if ($this->get('conditions')->isEmpty()) {
$conditions = $this->getConditions();
if (!$conditions) {
// Promotions without conditions always apply.
return TRUE;
}
$contexts = [
'commerce_promotion' => new Context(new ContextDefinition('entity:commerce_promotion'), $this),
];
// Execute each plugin, this is an AND operation.
// @todo support OR operations.
/** @var \Drupal\commerce\Plugin\Field\FieldType\PluginItem $item */
foreach ($this->get('conditions') as $item) {
$definition = $item->getTargetDefinition();
if ($definition['target_entity_type'] == 'commerce_order') {
/** @var \Drupal\commerce_promotion\Plugin\Commerce\PromotionCondition\PromotionConditionInterface $condition */
$condition = $item->getTargetInstance($contexts + [
'commerce_order' => new Context(new ContextDefinition('entity:commerce_order'), $order),
]);
if ($condition->evaluate()) {
return TRUE;
}
}
elseif ($definition['target_entity_type'] == 'commerce_order_item') {
foreach ($order->getItems() as $order_item) {
/** @var \Drupal\commerce_promotion\Plugin\Commerce\PromotionCondition\PromotionConditionInterface $condition */
$condition = $item->getTargetInstance($contexts + [
'commerce_order_item' => new Context(new ContextDefinition('entity:commerce_order_item'), $order_item),
]);
if ($condition->evaluate()) {
return TRUE;
}
}
$order_conditions = array_filter($conditions, function ($condition) {
/** @var \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface $condition */
return $condition->getEntityTypeId() == 'commerce_order';
});
$order_item_conditions = array_filter($conditions, function ($condition) {
/** @var \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface $condition */
return $condition->getEntityTypeId() == 'commerce_order_item';
});
$order_conditions = new ConditionGroup($order_conditions, 'AND');
$order_item_conditions = new ConditionGroup($order_item_conditions, 'AND');
if (!$order_conditions->evaluate($order)) {
return FALSE;
}
foreach ($order->getItems() as $order_item) {
// Order item conditions must match at least one order item.
if ($order_item_conditions->evaluate($order_item)) {
return TRUE;
}
}
......@@ -548,7 +551,7 @@ class Promotion extends ContentEntityBase implements PromotionInterface {
'weight' => 3,
]);
$fields['conditions'] = BaseFieldDefinition::create('commerce_plugin_item:commerce_promotion_condition')
$fields['conditions'] = BaseFieldDefinition::create('commerce_plugin_item:commerce_condition')
->setLabel(t('Conditions'))
->setCardinality(BaseFieldDefinition::CARDINALITY_UNLIMITED)
->setRequired(FALSE)
......
......@@ -87,6 +87,14 @@ interface PromotionInterface extends ContentEntityInterface, EntityStoresInterfa
*/
public function setOrderTypeIds(array $order_type_ids);
/**
* Gets the conditions.
*
* @return \Drupal\commerce\Plugin\Commerce\Condition\ConditionInterface[]
* The conditions.
*/
public function getConditions();
/**
* Gets the coupon IDs.
*
......
<?php
namespace Drupal\commerce_promotion\Plugin\Commerce\PromotionCondition;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformState;
/**
* Provides the base class for conditions.
*/
abstract class PromotionConditionBase extends ConditionPluginBase implements PromotionConditionInterface {
/**
* {@inheritdoc}
*/
public function getTargetEntityTypeId() {
return $this->pluginDefinition['target_entity_type'];
}
/**
* {@inheritdoc}
*/
public function getTargetEntity() {
return $this->getContextValue($this->getTargetEntityTypeId());
}
/**
* {@inheritdoc}
*/
public function execute() {
$result = $this->evaluate();
return $this->isNegated() ? !$result : $result;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
return parent::submitConfigurationForm($form, SubformState::createForSubform($form, $form_state->getCompleteForm(), $form_state));
}
/**
* Gets the comparison operators.
*
* @return array
* The comparison operators.
*/
protected function getComparisonOperators() {
return [
'>' => $this->t('Greater than'),
'>=' => $this->t('Greater than or equal to'),
'<=' => $this->t('Less than or equal to'),
'<' => $this->t('Less than'),
'==' => $this->t('Equals'),
];
}
}
<?php
namespace Drupal\commerce_promotion\Plugin\Commerce\PromotionCondition;
use Drupal\Core\Condition\ConditionInterface;
/**
* Defines an interface for Condition plugins.
*/
interface PromotionConditionInterface extends ConditionInterface {
/**
* Gets the entity type the condition is for.
*
* @return string
* The entity type it applies to.
*/
public function getTargetEntityTypeId();
/**
* Get the target entity for the offer.
*
* @return \Drupal\Core\Entity\EntityInterface
* The target entity.
*/
public function getTargetEntity();
}
......@@ -19,7 +19,7 @@ abstract class PercentageOffBase extends PromotionOfferBase {
}
/**
* Gets the percentage amount, as a decimal, negated.
* Gets the percentage amount.
*
* @return string
* The amount.
......
<?php
namespace Drupal\commerce_promotion_test\Plugin\Commerce\PromotionCondition;
use Drupal\commerce_promotion\Plugin\Commerce\PromotionCondition\PromotionConditionBase;
/**
* Provides a 'Product variation SKU' condition.
*
* @CommercePromotionCondition(
* id = "commerce_promotion_test_variant_sku",
* label = @Translation("Product SKU is TEST123"),
* target_entity_type = "commerce_order_item",
* )
*/
class TestSpecificSkuCondition extends PromotionConditionBase {
/**
* {@inheritdoc}
*/
public function evaluate() {
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
$order_item = $this->getTargetEntity();
/** @var \Drupal\commerce_product\Entity\ProductVariationInterface $purchased_entity */
$purchased_entity = $order_item->getPurchasedEntity();
return $purchased_entity->getSku() == 'TEST123';
}
/**
* {@inheritdoc}
*/
public function summary() {
return $this->t('Tests a specific hardcoded SKU');
}
}
......@@ -62,7 +62,6 @@ class PromotionTest extends CommerceBrowserTestBase {
$this->getSession()->getPage()->selectFieldOption('conditions[0][target_plugin_id]', 'commerce_promotion_order_total_price');