Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[5.3] Add coupon code condition rule and order query param #3777

Merged
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