Skip to content

Commit

Permalink
Merge pull request #3777 from craftcms/feature/pt-2315-add-coupon-cod…
Browse files Browse the repository at this point in the history
…e-order-condition-rule

[5.3] Add coupon code condition rule and order query param
  • Loading branch information
nfourtythree authored Dec 11, 2024
2 parents c8105c9 + dfd4460 commit 6b584c5
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 7 deletions.
9 changes: 5 additions & 4 deletions CHANGELOG-WIP.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
# Release Notes for Craft Commerce (WIP)

### Store Management

- Order conditions can now have a “Coupon Code” rule. ([#3776](https://github.com/craftcms/commerce/discussions/3776))
- Order conditions can now have a “Payment Gateway” rule. ([#3722](https://github.com/craftcms/commerce/discussions/3722))
- Variant conditions can now have a “Product” rule.

### Administration

- Added support for `to`, `bcc`, and `cc` email fields to support environment variables. ([#3738](https://github.com/craftcms/commerce/issues/3738))

### Development

- Added the `couponCode` order query param.
- Added an `originalCart` value to the `commerce/update-cart` failed ajax response. ([#430](https://github.com/craftcms/commerce/issues/430))

### Extensibility

- Added `craft\commerce\base\InventoryItemTrait`.
- Added `craft\commerce\base\InventoryLocationTrait`.
- Added `craft\commerce\elements\conditions\orders\CouponCodeConditionRule`.
- Added `craft\commerce\elements\conditions\variants\ProductConditionRule`.
- Added `craft\commerce\elements\db\OrderQuery::$couponCode`.
- Added `craft\commerce\elements\db\OrderQuery::couponCode()`.
- Added `craft\commerce\services\Inventory::updateInventoryLevel()`.
- Added `craft\commerce\services\Inventory::updatePurchasableInventoryLevel()`.
57 changes: 57 additions & 0 deletions src/elements/conditions/orders/CouponCodeConditionRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php
/**
* @link https://craftcms.com/
* @copyright Copyright (c) Pixel & Tonic, Inc.
* @license https://craftcms.github.io/license/
*/

namespace craft\commerce\elements\conditions\orders;

use Craft;
use craft\helpers\StringHelper;
use yii\base\InvalidConfigException;

/**
* Order Coupon Code condition rule.
*
* @author Pixel & Tonic, Inc. <[email protected]>
* @since 5.3.0
*/
class CouponCodeConditionRule extends OrderTextValuesAttributeConditionRule
{
public string $orderAttribute = 'couponCode';

/**
* @inheritdoc
*/
public function getLabel(): string
{
return Craft::t('commerce', 'Coupon Code');
}

/**
* @inheritdoc
*/
protected function matchValue(mixed $value): bool
{
switch ($this->operator) {
case self::OPERATOR_EMPTY:
return !$value;
case self::OPERATOR_NOT_EMPTY:
return (bool)$value;
}

if ($this->value === '') {
return true;
}

return match ($this->operator) {
self::OPERATOR_EQ => strcasecmp($value, $this->value) === 0,
self::OPERATOR_NE => strcasecmp($value, $this->value) !== 0,
self::OPERATOR_BEGINS_WITH => is_string($value) && StringHelper::startsWith($value, $this->value, false),
self::OPERATOR_ENDS_WITH => is_string($value) && StringHelper::endsWith($value, $this->value, false),
self::OPERATOR_CONTAINS => is_string($value) && StringHelper::contains($value, $this->value, false),
default => throw new InvalidConfigException("Invalid operator: $this->operator"),
};
}
}
8 changes: 7 additions & 1 deletion src/elements/conditions/orders/DiscountOrderCondition.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use craft\commerce\base\HasStoreInterface;
use craft\commerce\base\StoreTrait;
use craft\elements\db\ElementQueryInterface;
use craft\helpers\ArrayHelper;
use yii\base\NotSupportedException;

/**
Expand Down Expand Up @@ -41,7 +42,12 @@ protected function config(): array
*/
protected function selectableConditionRules(): array
{
return array_merge(parent::selectableConditionRules(), []);
$rules = array_merge(parent::selectableConditionRules(), []);

// We don't need the condition to have the coupon code rule
ArrayHelper::removeValue($rules, CouponCodeConditionRule::class);

return $rules;
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/elements/conditions/orders/OrderCondition.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ protected function selectableConditionRules(): array
{
return array_merge(parent::selectableConditionRules(), [
DateOrderedConditionRule::class,
CustomerConditionRule::class,
CompletedConditionRule::class,
CouponCodeConditionRule::class,
CustomerConditionRule::class,
PaidConditionRule::class,
HasPurchasableConditionRule::class,
ItemSubtotalConditionRule::class,
Expand Down
53 changes: 53 additions & 0 deletions src/elements/db/OrderQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ class OrderQuery extends ElementQuery
*/
public mixed $reference = null;

/**
* @var mixed The order reference of the resulting order.
* @used-by couponCode()
*/
public mixed $couponCode = null;

/**
* @var mixed The email address the resulting orders must have.
*/
Expand Down Expand Up @@ -372,6 +378,48 @@ public function reference(mixed $value): OrderQuery
return $this;
}

/**
* Narrows the query results based on the order's coupon code.
*
* Possible values include:
*
* | Value | Fetches {elements}…
* | - | -
* | `':empty:'` | that don’t have a coupon code.
* | `':notempty:'` | that have a coupon code.
* | `'Foo'` | with a coupon code of `Foo`.
* | `'Foo*'` | with a coupon code that begins with `Foo`.
* | `'*Foo'` | with a coupon code that ends with `Foo`.
* | `'*Foo*'` | with a coupon code that contains `Foo`.
* | `'not *Foo*'` | with a coupon code that doesn’t contain `Foo`.
* | `['*Foo*', '*Bar*']` | with a coupon code that contains `Foo` or `Bar`.
* | `['not', '*Foo*', '*Bar*']` | with a coupon code that doesn’t contain `Foo` or `Bar`.
*
* ---
*
* ```twig
* {# Fetch the requested {element} #}
* {% set {element-var} = {twig-method}
* .reference('foo')
* .one() %}
* ```
*
* ```php
* // Fetch the requested {element}
* ${element-var} = {php-method}
* ->reference('foo')
* ->one();
* ```
*
* @param string|null $value The property value
* @return static self reference
*/
public function couponCode(mixed $value): OrderQuery
{
$this->couponCode = $value;
return $this;
}

/**
* Narrows the query results based on the customers’ email addresses.
*
Expand Down Expand Up @@ -1602,6 +1650,11 @@ protected function beforePrepare(): bool
$this->subQuery->andWhere(Db::parseParam('commerce_orders.reference', $this->reference));
}

if (isset($this->couponCode)) {
// Coupon code criteria is case-insensitive like in the adjuster
$this->subQuery->andWhere(Db::parseParam('commerce_orders.couponCode', $this->couponCode, caseInsensitive: true));
}

if (isset($this->email) && $this->email) {
// Join and search the users table for email address
$this->subQuery->leftJoin(CraftTable::USERS . ' users', '[[users.id]] = [[commerce_orders.customerId]]');
Expand Down
39 changes: 39 additions & 0 deletions tests/unit/elements/order/OrderQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,45 @@ public function emailDataProvider(): array
];
}

/**
* @param string $couponCode
* @param int $count
* @return void
* @dataProvider couponCodeDataProvider
*/
public function testCouponCode(?string $couponCode, int $count): void
{
$ordersFixture = $this->tester->grabFixture('orders');
/** @var Order $order */
$order = $ordersFixture->getElement('completed-new');

// Temporarily add a coupon code to an order
\craft\commerce\records\Order::updateAll(['couponCode' => 'foo'], ['id' => $order->id]);

$orderQuery = Order::find();
$orderQuery->couponCode($couponCode);

self::assertCount($count, $orderQuery->all());

// Remove temporary coupon code
\craft\commerce\records\Order::updateAll(['couponCode' => null], ['id' => $order->id]);
}

/**
* @return array[]
*/
public function couponCodeDataProvider(): array
{
return [
'normal' => ['foo', 1],
'case-insensitive' => ['fOo', 1],
'using-null' => [null, 3],
'empty-code' => [':empty:', 2],
'not-empty-code' => [':notempty:', 1],
'no-results' => ['nope', 0],
];
}

/**
* @param mixed $handle
* @param int $count
Expand Down
Loading

0 comments on commit 6b584c5

Please sign in to comment.