Skip to content

Commit 6b584c5

Browse files
authored
Merge pull request #3777 from craftcms/feature/pt-2315-add-coupon-code-order-condition-rule
[5.3] Add coupon code condition rule and order query param
2 parents c8105c9 + dfd4460 commit 6b584c5

File tree

8 files changed

+328
-7
lines changed

8 files changed

+328
-7
lines changed

CHANGELOG-WIP.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
# Release Notes for Craft Commerce (WIP)
22

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

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

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

1615
### Extensibility
17-
1816
- Added `craft\commerce\base\InventoryItemTrait`.
1917
- Added `craft\commerce\base\InventoryLocationTrait`.
18+
- Added `craft\commerce\elements\conditions\orders\CouponCodeConditionRule`.
2019
- Added `craft\commerce\elements\conditions\variants\ProductConditionRule`.
20+
- Added `craft\commerce\elements\db\OrderQuery::$couponCode`.
21+
- Added `craft\commerce\elements\db\OrderQuery::couponCode()`.
2122
- Added `craft\commerce\services\Inventory::updateInventoryLevel()`.
2223
- Added `craft\commerce\services\Inventory::updatePurchasableInventoryLevel()`.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
/**
3+
* @link https://craftcms.com/
4+
* @copyright Copyright (c) Pixel & Tonic, Inc.
5+
* @license https://craftcms.github.io/license/
6+
*/
7+
8+
namespace craft\commerce\elements\conditions\orders;
9+
10+
use Craft;
11+
use craft\helpers\StringHelper;
12+
use yii\base\InvalidConfigException;
13+
14+
/**
15+
* Order Coupon Code condition rule.
16+
*
17+
* @author Pixel & Tonic, Inc. <support@pixelandtonic.com>
18+
* @since 5.3.0
19+
*/
20+
class CouponCodeConditionRule extends OrderTextValuesAttributeConditionRule
21+
{
22+
public string $orderAttribute = 'couponCode';
23+
24+
/**
25+
* @inheritdoc
26+
*/
27+
public function getLabel(): string
28+
{
29+
return Craft::t('commerce', 'Coupon Code');
30+
}
31+
32+
/**
33+
* @inheritdoc
34+
*/
35+
protected function matchValue(mixed $value): bool
36+
{
37+
switch ($this->operator) {
38+
case self::OPERATOR_EMPTY:
39+
return !$value;
40+
case self::OPERATOR_NOT_EMPTY:
41+
return (bool)$value;
42+
}
43+
44+
if ($this->value === '') {
45+
return true;
46+
}
47+
48+
return match ($this->operator) {
49+
self::OPERATOR_EQ => strcasecmp($value, $this->value) === 0,
50+
self::OPERATOR_NE => strcasecmp($value, $this->value) !== 0,
51+
self::OPERATOR_BEGINS_WITH => is_string($value) && StringHelper::startsWith($value, $this->value, false),
52+
self::OPERATOR_ENDS_WITH => is_string($value) && StringHelper::endsWith($value, $this->value, false),
53+
self::OPERATOR_CONTAINS => is_string($value) && StringHelper::contains($value, $this->value, false),
54+
default => throw new InvalidConfigException("Invalid operator: $this->operator"),
55+
};
56+
}
57+
}

src/elements/conditions/orders/DiscountOrderCondition.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use craft\commerce\base\HasStoreInterface;
66
use craft\commerce\base\StoreTrait;
77
use craft\elements\db\ElementQueryInterface;
8+
use craft\helpers\ArrayHelper;
89
use yii\base\NotSupportedException;
910

1011
/**
@@ -41,7 +42,12 @@ protected function config(): array
4142
*/
4243
protected function selectableConditionRules(): array
4344
{
44-
return array_merge(parent::selectableConditionRules(), []);
45+
$rules = array_merge(parent::selectableConditionRules(), []);
46+
47+
// We don't need the condition to have the coupon code rule
48+
ArrayHelper::removeValue($rules, CouponCodeConditionRule::class);
49+
50+
return $rules;
4551
}
4652

4753
/**

src/elements/conditions/orders/OrderCondition.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ protected function selectableConditionRules(): array
3030
{
3131
return array_merge(parent::selectableConditionRules(), [
3232
DateOrderedConditionRule::class,
33-
CustomerConditionRule::class,
3433
CompletedConditionRule::class,
34+
CouponCodeConditionRule::class,
35+
CustomerConditionRule::class,
3536
PaidConditionRule::class,
3637
HasPurchasableConditionRule::class,
3738
ItemSubtotalConditionRule::class,

src/elements/db/OrderQuery.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ class OrderQuery extends ElementQuery
5959
*/
6060
public mixed $reference = null;
6161

62+
/**
63+
* @var mixed The order reference of the resulting order.
64+
* @used-by couponCode()
65+
*/
66+
public mixed $couponCode = null;
67+
6268
/**
6369
* @var mixed The email address the resulting orders must have.
6470
*/
@@ -372,6 +378,48 @@ public function reference(mixed $value): OrderQuery
372378
return $this;
373379
}
374380

381+
/**
382+
* Narrows the query results based on the order's coupon code.
383+
*
384+
* Possible values include:
385+
*
386+
* | Value | Fetches {elements}…
387+
* | - | -
388+
* | `':empty:'` | that don’t have a coupon code.
389+
* | `':notempty:'` | that have a coupon code.
390+
* | `'Foo'` | with a coupon code of `Foo`.
391+
* | `'Foo*'` | with a coupon code that begins with `Foo`.
392+
* | `'*Foo'` | with a coupon code that ends with `Foo`.
393+
* | `'*Foo*'` | with a coupon code that contains `Foo`.
394+
* | `'not *Foo*'` | with a coupon code that doesn’t contain `Foo`.
395+
* | `['*Foo*', '*Bar*']` | with a coupon code that contains `Foo` or `Bar`.
396+
* | `['not', '*Foo*', '*Bar*']` | with a coupon code that doesn’t contain `Foo` or `Bar`.
397+
*
398+
* ---
399+
*
400+
* ```twig
401+
* {# Fetch the requested {element} #}
402+
* {% set {element-var} = {twig-method}
403+
* .reference('foo')
404+
* .one() %}
405+
* ```
406+
*
407+
* ```php
408+
* // Fetch the requested {element}
409+
* ${element-var} = {php-method}
410+
* ->reference('foo')
411+
* ->one();
412+
* ```
413+
*
414+
* @param string|null $value The property value
415+
* @return static self reference
416+
*/
417+
public function couponCode(mixed $value): OrderQuery
418+
{
419+
$this->couponCode = $value;
420+
return $this;
421+
}
422+
375423
/**
376424
* Narrows the query results based on the customers’ email addresses.
377425
*
@@ -1602,6 +1650,11 @@ protected function beforePrepare(): bool
16021650
$this->subQuery->andWhere(Db::parseParam('commerce_orders.reference', $this->reference));
16031651
}
16041652

1653+
if (isset($this->couponCode)) {
1654+
// Coupon code criteria is case-insensitive like in the adjuster
1655+
$this->subQuery->andWhere(Db::parseParam('commerce_orders.couponCode', $this->couponCode, caseInsensitive: true));
1656+
}
1657+
16051658
if (isset($this->email) && $this->email) {
16061659
// Join and search the users table for email address
16071660
$this->subQuery->leftJoin(CraftTable::USERS . ' users', '[[users.id]] = [[commerce_orders.customerId]]');

tests/unit/elements/order/OrderQueryTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,45 @@ public function emailDataProvider(): array
6363
];
6464
}
6565

66+
/**
67+
* @param string $couponCode
68+
* @param int $count
69+
* @return void
70+
* @dataProvider couponCodeDataProvider
71+
*/
72+
public function testCouponCode(?string $couponCode, int $count): void
73+
{
74+
$ordersFixture = $this->tester->grabFixture('orders');
75+
/** @var Order $order */
76+
$order = $ordersFixture->getElement('completed-new');
77+
78+
// Temporarily add a coupon code to an order
79+
\craft\commerce\records\Order::updateAll(['couponCode' => 'foo'], ['id' => $order->id]);
80+
81+
$orderQuery = Order::find();
82+
$orderQuery->couponCode($couponCode);
83+
84+
self::assertCount($count, $orderQuery->all());
85+
86+
// Remove temporary coupon code
87+
\craft\commerce\records\Order::updateAll(['couponCode' => null], ['id' => $order->id]);
88+
}
89+
90+
/**
91+
* @return array[]
92+
*/
93+
public function couponCodeDataProvider(): array
94+
{
95+
return [
96+
'normal' => ['foo', 1],
97+
'case-insensitive' => ['fOo', 1],
98+
'using-null' => [null, 3],
99+
'empty-code' => [':empty:', 2],
100+
'not-empty-code' => [':notempty:', 1],
101+
'no-results' => ['nope', 0],
102+
];
103+
}
104+
66105
/**
67106
* @param mixed $handle
68107
* @param int $count

0 commit comments

Comments
 (0)